diff --git a/Cargo.toml b/Cargo.toml index a6bf453..0515a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "color-spantrace", "eyre" ] diff --git a/color-spantrace/.gitignore b/color-spantrace/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/color-spantrace/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/color-spantrace/CHANGELOG.md b/color-spantrace/CHANGELOG.md new file mode 100644 index 0000000..3014914 --- /dev/null +++ b/color-spantrace/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + +## [Unreleased] - ReleaseDate + +## [0.2.0] - 2022-01-12 +### Changed +- Updated dependency versions to match latest tracing versions + +## [0.1.6] - 2020-12-02 +### Fixed +- Ignore all io errors when resolving source files instead of only file not + found errors + +## [v0.1.5] - 2020-12-01 +### Added +- Support custom color themes for spantrace format + + +[Unreleased]: https://github.com/eyre-rs/color-spantrace/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/eyre-rs/color-spantrace/compare/v0.1.6...v0.2.0 +[0.1.6]: https://github.com/eyre-rs/color-spantrace/compare/v0.1.5...v0.1.6 +[v0.1.5]: https://github.com/eyre-rs/color-spantrace/releases/tag/v0.1.5 diff --git a/color-spantrace/Cargo.toml b/color-spantrace/Cargo.toml new file mode 100644 index 0000000..e1510d8 --- /dev/null +++ b/color-spantrace/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "color-spantrace" +version = "0.2.0" +description = "A pretty printer for tracing_error::SpanTrace based on color-backtrace" +documentation = "https://docs.rs/color-spantrace" + +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +readme = { workspace = true } +rust-version = { workspace = true } + +[dependencies] +tracing-error = "0.2.0" +tracing-core = "0.1.21" +owo-colors = "3.2.0" +once_cell = { workspace = true } + +[dev-dependencies] +tracing-subscriber = "0.3.4" +tracing = "0.1.29" +ansi-parser = "0.8" # used for testing color schemes + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.release] +dev-version = false + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "Unreleased" +replace="{{version}}" + +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = "#!\\[doc\\(html_root_url.*" +replace = "#![doc(html_root_url = \"https://docs.rs/{{crate_name}}/{{version}}\")]" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "\\.\\.\\.HEAD" +replace="...{{tag_name}}" +exactly = 1 + +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "ReleaseDate" +replace="{{date}}" + +[[package.metadata.release.pre-release-replacements]] +file="CHANGELOG.md" +search="" +replace="\n\n## [Unreleased] - ReleaseDate" +exactly=1 + +[[package.metadata.release.pre-release-replacements]] +file="CHANGELOG.md" +search="" +replace="\n[Unreleased]: https://github.com/eyre-rs/{{crate_name}}/compare/{{tag_name}}...HEAD" +exactly=1 diff --git a/color-spantrace/LICENSE-APACHE b/color-spantrace/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/color-spantrace/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/color-spantrace/LICENSE-MIT b/color-spantrace/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/color-spantrace/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/color-spantrace/README.md b/color-spantrace/README.md new file mode 100644 index 0000000..a2eaec9 --- /dev/null +++ b/color-spantrace/README.md @@ -0,0 +1,110 @@ +color-spantrace +=============== + +[![Build Status][actions-badge]][actions-url] +[![Latest Version](https://img.shields.io/crates/v/color-spantrace.svg)](https://crates.io/crates/color-spantrace) +[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/color-spantrace) + +[actions-badge]: https://github.com/eyre-rs/color-spantrace/workflows/Continuous%20integration/badge.svg +[actions-url]: https://github.com/eyre-rs/color-spantrace/actions?query=workflow%3A%22Continuous+integration%22 + +A rust library for colorizing [`tracing_error::SpanTrace`] objects in the style +of [`color-backtrace`]. + +## Setup + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +color-spantrace = "0.2" +tracing = "0.1" +tracing-error = "0.2" +tracing-subscriber = "0.3" +``` + +Setup a tracing subscriber with an `ErrorLayer`: + +```rust +use tracing_error::ErrorLayer; +use tracing_subscriber::{prelude::*, registry::Registry}; + +Registry::default().with(ErrorLayer::default()).init(); +``` + +Create spans and enter them: + +```rust +use tracing::instrument; +use tracing_error::SpanTrace; + +#[instrument] +fn foo() -> SpanTrace { + SpanTrace::capture() +} +``` + +And finally colorize the `SpanTrace`: + +```rust +use tracing_error::SpanTrace; + +let span_trace = SpanTrace::capture(); +println!("{}", color_spantrace::colorize(&span_trace)); +``` + +## Example + +This example is taken from `examples/usage.rs`: + +```rust +use tracing::instrument; +use tracing_error::{ErrorLayer, SpanTrace}; +use tracing_subscriber::{prelude::*, registry::Registry}; + +#[instrument] +fn main() { + Registry::default().with(ErrorLayer::default()).init(); + + let span_trace = one(42); + println!("{}", color_spantrace::colorize(&span_trace)); +} + +#[instrument] +fn one(i: u32) -> SpanTrace { + two() +} + +#[instrument] +fn two() -> SpanTrace { + SpanTrace::capture() +} +``` + +This creates the following output + +### Minimal Format + +![minimal format](./pictures/minimal.png) + +### Full Format + +![Full format](./pictures/full.png) + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + + +[`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html +[`color-backtrace`]: https://github.com/athre0z/color-backtrace diff --git a/color-spantrace/examples/usage.rs b/color-spantrace/examples/usage.rs new file mode 100644 index 0000000..21cca48 --- /dev/null +++ b/color-spantrace/examples/usage.rs @@ -0,0 +1,21 @@ +use tracing::instrument; +use tracing_error::{ErrorLayer, SpanTrace}; +use tracing_subscriber::{prelude::*, registry::Registry}; + +#[instrument] +fn main() { + Registry::default().with(ErrorLayer::default()).init(); + + let span_trace = one(42); + println!("{}", color_spantrace::colorize(&span_trace)); +} + +#[instrument] +fn one(i: u32) -> SpanTrace { + two() +} + +#[instrument] +fn two() -> SpanTrace { + SpanTrace::capture() +} diff --git a/color-spantrace/pictures/full.png b/color-spantrace/pictures/full.png new file mode 100644 index 0000000..489da11 Binary files /dev/null and b/color-spantrace/pictures/full.png differ diff --git a/color-spantrace/pictures/minimal.png b/color-spantrace/pictures/minimal.png new file mode 100644 index 0000000..7d3dfd6 Binary files /dev/null and b/color-spantrace/pictures/minimal.png differ diff --git a/color-spantrace/src/lib.rs b/color-spantrace/src/lib.rs new file mode 100644 index 0000000..7d51313 --- /dev/null +++ b/color-spantrace/src/lib.rs @@ -0,0 +1,376 @@ +//! A rust library for colorizing [`tracing_error::SpanTrace`] objects in the style +//! of [`color-backtrace`]. +//! +//! ## Setup +//! +//! Add the following to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! color-spantrace = "0.2" +//! tracing = "0.1" +//! tracing-error = "0.2" +//! tracing-subscriber = "0.3" +//! ``` +//! +//! Setup a tracing subscriber with an `ErrorLayer`: +//! +//! ```rust +//! use tracing_error::ErrorLayer; +//! use tracing_subscriber::{prelude::*, registry::Registry}; +//! +//! Registry::default().with(ErrorLayer::default()).init(); +//! ``` +//! +//! Create spans and enter them: +//! +//! ```rust +//! use tracing::instrument; +//! use tracing_error::SpanTrace; +//! +//! #[instrument] +//! fn foo() -> SpanTrace { +//! SpanTrace::capture() +//! } +//! ``` +//! +//! And finally colorize the `SpanTrace`: +//! +//! ```rust +//! use tracing_error::SpanTrace; +//! +//! let span_trace = SpanTrace::capture(); +//! println!("{}", color_spantrace::colorize(&span_trace)); +//! ``` +//! +//! ## Output Format +//! +//! Running `examples/usage.rs` from the `color-spantrace` repo produces the following output: +//! +//!
 cargo run --example usage
+//!     Finished dev [unoptimized + debuginfo] target(s) in 0.04s
+//!      Running `target/debug/examples/usage`
+//! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+//!
+//!  0: usage::two
+//!     at examples/usage.rs:18
+//!  1: usage::one with i=42
+//!     at examples/usage.rs:13
+//! +//! [`tracing_error::SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html +//! [`color-backtrace`]: https://github.com/athre0z/color-backtrace +#![doc(html_root_url = "https://docs.rs/color-spantrace/0.2.0")] +#![warn( + missing_debug_implementations, + missing_docs, + rustdoc::missing_doc_code_examples, + rust_2018_idioms, + unreachable_pub, + bad_style, + dead_code, + improper_ctypes, + non_shorthand_field_patterns, + no_mangle_generic_items, + overflowing_literals, + path_statements, + patterns_in_fns_without_body, + private_in_public, + unconditional_recursion, + unused, + unused_allocation, + unused_comparisons, + unused_parens, + while_true +)] +use once_cell::sync::OnceCell; +use owo_colors::{style, Style}; +use std::env; +use std::fmt; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use tracing_error::SpanTrace; + +static THEME: OnceCell = OnceCell::new(); + +/// A struct that represents theme that is used by `color_spantrace` +#[derive(Debug, Copy, Clone, Default)] +pub struct Theme { + file: Style, + line_number: Style, + target: Style, + fields: Style, + active_line: Style, +} + +impl Theme { + /// Create blank theme + pub fn new() -> Self { + Self::default() + } + + /// A theme for a dark background. This is the default + pub fn dark() -> Self { + Self { + file: style().purple(), + line_number: style().purple(), + active_line: style().white().bold(), + target: style().bright_red(), + fields: style().bright_cyan(), + } + } + + // XXX same as with `light` in `color_eyre` + /// A theme for a light background + pub fn light() -> Self { + Self { + file: style().purple(), + line_number: style().purple(), + target: style().red(), + fields: style().blue(), + active_line: style().bold(), + } + } + + /// Styles printed paths + pub fn file(mut self, style: Style) -> Self { + self.file = style; + self + } + + /// Styles the line number of a file + pub fn line_number(mut self, style: Style) -> Self { + self.line_number = style; + self + } + + /// Styles the target (i.e. the module and function name, and so on) + pub fn target(mut self, style: Style) -> Self { + self.target = style; + self + } + + /// Styles fields associated with a the `tracing::Span`. + pub fn fields(mut self, style: Style) -> Self { + self.fields = style; + self + } + + /// Styles the selected line of displayed code + pub fn active_line(mut self, style: Style) -> Self { + self.active_line = style; + self + } +} + +/// An error returned by `set_theme` if a global theme was already set +#[derive(Debug)] +pub struct InstallThemeError; + +impl fmt::Display for InstallThemeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("could not set the provided `Theme` globally as another was already set") + } +} + +impl std::error::Error for InstallThemeError {} + +/// Sets the global theme. +/// +/// # Details +/// +/// This can only be set once and otherwise fails. +/// +/// **Note:** `colorize` sets the global theme implicitly, if it was not set already. So calling `colorize` and then `set_theme` fails +pub fn set_theme(theme: Theme) -> Result<(), InstallThemeError> { + THEME.set(theme).map_err(|_| InstallThemeError) +} + +/// Display a [`SpanTrace`] with colors and source +/// +/// This function returns an `impl Display` type which can be then used in place of the original +/// SpanTrace when writing it too the screen or buffer. +/// +/// # Example +/// +/// ```rust +/// use tracing_error::SpanTrace; +/// +/// let span_trace = SpanTrace::capture(); +/// println!("{}", color_spantrace::colorize(&span_trace)); +/// ``` +/// +/// **Note:** `colorize` sets the global theme implicitly, if it was not set already. So calling `colorize` and then `set_theme` fails +/// +/// [`SpanTrace`]: https://docs.rs/tracing-error/*/tracing_error/struct.SpanTrace.html +pub fn colorize(span_trace: &SpanTrace) -> impl fmt::Display + '_ { + let theme = *THEME.get_or_init(Theme::dark); + ColorSpanTrace { span_trace, theme } +} + +struct ColorSpanTrace<'a> { + span_trace: &'a SpanTrace, + theme: Theme, +} + +macro_rules! try_bool { + ($e:expr, $dest:ident) => {{ + let ret = $e.unwrap_or_else(|e| $dest = Err(e)); + + if $dest.is_err() { + return false; + } + + ret + }}; +} + +struct Frame<'a> { + metadata: &'a tracing_core::Metadata<'static>, + fields: &'a str, + theme: Theme, +} + +/// Defines how verbose the backtrace is supposed to be. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Verbosity { + /// Print a small message including the panic payload and the panic location. + Minimal, + /// Everything in `Minimal` and additionally print a backtrace. + Medium, + /// Everything in `Medium` plus source snippets for all backtrace locations. + Full, +} + +impl Verbosity { + fn lib_from_env() -> Self { + Self::convert_env( + env::var("RUST_LIB_BACKTRACE") + .or_else(|_| env::var("RUST_BACKTRACE")) + .ok(), + ) + } + + fn convert_env(env: Option) -> Self { + match env { + Some(ref x) if x == "full" => Verbosity::Full, + Some(_) => Verbosity::Medium, + None => Verbosity::Minimal, + } + } +} + +impl Frame<'_> { + fn print(&self, i: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.print_header(i, f)?; + self.print_fields(f)?; + self.print_source_location(f)?; + Ok(()) + } + + fn print_header(&self, i: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:>2}: {}{}{}", + i, + self.theme.target.style(self.metadata.target()), + self.theme.target.style("::"), + self.theme.target.style(self.metadata.name()), + ) + } + + fn print_fields(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.fields.is_empty() { + write!(f, " with {}", self.theme.fields.style(self.fields))?; + } + + Ok(()) + } + + fn print_source_location(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(file) = self.metadata.file() { + let lineno = self + .metadata + .line() + .map_or("".to_owned(), |x| x.to_string()); + write!( + f, + "\n at {}:{}", + self.theme.file.style(file), + self.theme.line_number.style(lineno), + )?; + } else { + write!(f, "\n at ")?; + } + + Ok(()) + } + + fn print_source_if_avail(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (lineno, filename) = match (self.metadata.line(), self.metadata.file()) { + (Some(a), Some(b)) => (a, b), + // Without a line number and file name, we can't sensibly proceed. + _ => return Ok(()), + }; + + let file = match File::open(filename) { + Ok(file) => file, + // ignore io errors and just don't print the source + Err(_) => return Ok(()), + }; + + use std::fmt::Write; + + // Extract relevant lines. + let reader = BufReader::new(file); + let start_line = lineno - 2.min(lineno - 1); + let surrounding_src = reader.lines().skip(start_line as usize - 1).take(5); + let mut buf = String::new(); + for (line, cur_line_no) in surrounding_src.zip(start_line..) { + if cur_line_no == lineno { + write!( + &mut buf, + "{:>8} > {}", + cur_line_no.to_string(), + line.unwrap() + )?; + write!(f, "\n{}", self.theme.active_line.style(&buf))?; + buf.clear(); + } else { + write!(f, "\n{:>8} │ {}", cur_line_no, line.unwrap())?; + } + } + + Ok(()) + } +} + +impl fmt::Display for ColorSpanTrace<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut err = Ok(()); + let mut span = 0; + + writeln!(f, "{:━^80}\n", " SPANTRACE ")?; + self.span_trace.with_spans(|metadata, fields| { + let frame = Frame { + metadata, + fields, + theme: self.theme, + }; + + if span > 0 { + try_bool!(write!(f, "\n",), err); + } + + try_bool!(frame.print(span, f), err); + + if Verbosity::lib_from_env() == Verbosity::Full { + try_bool!(frame.print_source_if_avail(f), err); + } + + span += 1; + true + }); + + err + } +} diff --git a/color-spantrace/tests/data/theme_control.txt b/color-spantrace/tests/data/theme_control.txt new file mode 100644 index 0000000..96d8830 --- /dev/null +++ b/color-spantrace/tests/data/theme_control.txt @@ -0,0 +1,9 @@ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + 0: themes::test_capture with x=42 + at tests/themes.rs:42 + 40 │ use tracing_subscriber::{prelude::*, registry::Registry}; + 41 │ + 42 > #[instrument] + 43 │ fn test_capture(x: u8) -> SpanTrace { + 44 │ #[allow(clippy::if_same_then_else)] \ No newline at end of file diff --git a/color-spantrace/tests/themes.rs b/color-spantrace/tests/themes.rs new file mode 100644 index 0000000..2f24d63 --- /dev/null +++ b/color-spantrace/tests/themes.rs @@ -0,0 +1,104 @@ +/* + + # How this test works: + + 1) generate a spantrace with `test_capture` + + 2) convert the spantrace to a string + + 3) load stored spantrace control to compare to spantrace string (stored in the path of `control_file_path` below) + + 4) if `control_file_path` doesn't exist, generate corresponding file in the current working directory and request the user to fix the issue (see below) + + 5) extract ANSI escaping sequences (of control and current spantrace) + + 6) compare if the current spantrace and the control contains the same ANSI escape sequences + + 7) If not, fail and show the full strings of the control and the current spantrace + + # Re-generating the control + + If the control spantrace is lost and/or it needs to be re-generated, do the following: + + 1) Checkout the `color_spantrace` version from Git that you want to test against + + 3) Add this test file to '/tests' + + 4) If `control_file_path` exist, delete it + + 5) If you now run this test, it will generate a test control file in the current working directory + + 6) copy this file to `control_file_path` (see instructions that are shown) + +*/ + +use tracing::instrument; +use tracing_error::SpanTrace; + +#[instrument] +fn test_capture(x: u8) -> SpanTrace { + #[allow(clippy::if_same_then_else)] + if x == 42 { + SpanTrace::capture() + } else { + SpanTrace::capture() + } +} + +#[cfg(not(miri))] +#[test] +fn test_backwards_compatibility() { + use ansi_parser::{AnsiParser, AnsiSequence, Output}; + use std::{fs, path::Path}; + use tracing_error::ErrorLayer; + use tracing_subscriber::{prelude::*, registry::Registry}; + std::env::set_var("RUST_LIB_BACKTRACE", "full"); + + // This integration is ran by cargo with cwd="color-spantrace", but the string literals for + // `file!` are relative to the workspace root. This changes the cwd to the workspace root. + // + // The behavior of file! when invoked from a workspace is not documented. See: . + // + // Noteworthy: non-member path dependencies will get an absolute path, as will registry and git + // dependencies. + std::env::set_current_dir("..").unwrap(); + + Registry::default().with(ErrorLayer::default()).init(); + + let spantrace = test_capture(42); + let colored_spantrace = format!("{}", color_spantrace::colorize(&spantrace)); + + let control_file_name = "theme_control.txt"; + let control_file_path = ["color-spantrace/tests/data/", control_file_name].concat(); + + // If `control_file_path` is missing, save corresponding file to current working directory, and panic with the request to move these files to `control_file_path`, and to commit them to Git. Being explicit (instead of saving directly to `control_file_path` to make sure `control_file_path` is committed to Git. These files anyway should never be missing. + + if !Path::new(&control_file_path).is_file() { + std::fs::write(control_file_name, &colored_spantrace) + .expect("\n\nError saving `colored_spantrace` to a file"); + panic!("Required test data missing! Fix this, by moving '{}' to '{}', and commit it to Git.\n\nNote: '{0}' was just generated in the current working directory.\n\n", control_file_name, control_file_path); + } + + // `unwrap` should never fail with files generated by this test + let colored_spantrace_control = + String::from_utf8(fs::read(control_file_path).unwrap()).unwrap(); + + fn get_ansi(s: &str) -> impl Iterator + '_ { + s.ansi_parse().filter_map(|x| { + if let Output::Escape(ansi) = x { + Some(ansi) + } else { + None + } + }) + } + + let colored_spantrace_ansi = get_ansi(&colored_spantrace); + let colored_spantrace_control_ansi = get_ansi(&colored_spantrace_control); + + assert!( + colored_spantrace_ansi.eq(colored_spantrace_control_ansi), + "\x1b[0mANSI escape sequences are not identical to control!\n\nCONTROL:\n\n{}\n\n\n\n{:?}\n\nCURRENT:\n\n{}\n\n\n\n{:?}\n\n", &colored_spantrace_control, &colored_spantrace_control, &colored_spantrace, &colored_spantrace + // `\x1b[0m` clears previous ANSI escape sequences + ); +}