Implement github issue generator (#62)

* Initial implementation of github issue generator

* document the hookbuilder methods

* fix doctest

* make issue generation feature optional

* update changelog

* (cargo-release) version 0.5.4-rc.1

* add doc cfg attr to new methods

* (cargo-release) version 0.5.4-rc.2

* supress issue sections when they'd be empty

* (cargo-release) version 0.5.4-rc.3

* (cargo-release) version 0.5.4

* (cargo-release) start next development iteration 0.5.5-alpha.0
This commit is contained in:
Jane Lusby 2020-09-17 14:14:38 -07:00 committed by GitHub
parent 05809302b3
commit 9c738c3a39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 507 additions and 15 deletions

View File

@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] - ReleaseDate
## [0.5.4] - 2020-09-17
### Added
- Add new "issue-url" feature for generating issue creation links in error
reports pre-populated with information about the error
## [0.5.3] - 2020-09-14
### Added
- add `panic_section` method to `HookBuilder` for overriding the printer for
@ -25,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
better compatibility with the Display trait
<!-- next-url -->
[Unreleased]: https://github.com/yaahc/color-eyre/compare/v0.5.3...HEAD
[Unreleased]: https://github.com/yaahc/color-eyre/compare/v0.5.4...HEAD
[0.5.4]: https://github.com/yaahc/color-eyre/compare/v0.5.3...v0.5.4
[0.5.3]: https://github.com/yaahc/color-eyre/compare/v0.5.2...v0.5.3
[0.5.2]: https://github.com/yaahc/color-eyre/releases/tag/v0.5.2

View File

@ -1,6 +1,6 @@
[package]
name = "color-eyre"
version = "0.5.4-alpha.0"
version = "0.5.5-alpha.0"
authors = ["Jane Lusby <jlusby@yaah.dev>"]
edition = "2018"
license = "MIT OR Apache-2.0"
@ -14,6 +14,7 @@ keywords = []
[features]
default = ["capture-spantrace"]
capture-spantrace = ["tracing-error", "color-spantrace"]
issue-url = ["url"]
[dependencies]
eyre = "0.6.0"
@ -23,6 +24,7 @@ indenter = "0.3.0"
owo-colors = "1.0.3"
color-spantrace = { version = "0.1.4", optional = true }
once_cell = "1.4.0"
url = { version = "2.1.1", optional = true }
[dev-dependencies]
tracing-subscriber = "0.2.5"

70
examples/github_issue.rs Normal file
View File

@ -0,0 +1,70 @@
#![allow(dead_code, unused_imports)]
use color_eyre::eyre;
use eyre::{Report, Result};
use tracing::instrument;
#[instrument]
#[cfg(feature = "issue-url")]
fn main() -> Result<(), Report> {
#[cfg(feature = "capture-spantrace")]
install_tracing();
color_eyre::config::HookBuilder::default()
.issue_url("https://github.com/yaahc/jane-eyre/issues/new")
.add_issue_metadata("version", "0.1.0")
.install()?;
let report = read_config().unwrap_err();
eprintln!("Error: {:?}", report);
read_config2();
Ok(())
}
#[cfg(not(feature = "issue-url"))]
fn main() {
unimplemented!("this example requires the \"issue-url\" feature")
}
#[cfg(feature = "capture-spantrace")]
fn install_tracing() {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
let fmt_layer = fmt::layer().with_target(false);
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.init();
}
#[instrument]
fn read_file(path: &str) -> Result<String> {
Ok(std::fs::read_to_string(path)?)
}
#[instrument]
fn read_config() -> Result<()> {
read_file("fake_file")?;
Ok(())
}
#[instrument]
fn read_file2(path: &str) {
if let Err(e) = std::fs::read_to_string(path) {
panic!("{}", e);
}
}
#[instrument]
fn read_config2() {
read_file2("fake_file")
}

View File

@ -250,6 +250,10 @@ pub struct HookBuilder {
display_env_section: bool,
panic_section: Option<Box<dyn Display + Send + Sync + 'static>>,
panic_message: Box<dyn PanicMessage>,
#[cfg(feature = "issue-url")]
issue_url: Option<String>,
#[cfg(feature = "issue-url")]
issue_metadata: Vec<(String, Box<dyn Display + Send + Sync + 'static>)>,
}
impl HookBuilder {
@ -284,6 +288,10 @@ impl HookBuilder {
display_env_section: true,
panic_section: None,
panic_message: Box::new(DefaultPanicMessage),
#[cfg(feature = "issue-url")]
issue_url: None,
#[cfg(feature = "issue-url")]
issue_metadata: vec![],
}
}
@ -360,6 +368,57 @@ impl HookBuilder {
self
}
/// Set an upstream github repo and enable issue reporting url generation
///
/// # Details
///
/// Once enabled, color-eyre will generate urls that will create customized
/// issues pre-populated with information about the associated error report.
///
/// Additional information can be added to the metadata table in the
/// generated urls by calling `add_issue_metadata` when configuring the
/// HookBuilder.
///
/// # Examples
///
/// ```rust
/// color_eyre::config::HookBuilder::default()
/// .issue_url("https://github.com/yaahc/jane-eyre/issues/new")
/// .install()
/// .unwrap();
/// ```
#[cfg(feature = "issue-url")]
#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
pub fn issue_url<S: ToString>(mut self, url: S) -> Self {
self.issue_url = Some(url.to_string());
self
}
/// Add a new entry to the metadata table in generated github issue urls
///
/// **Note**: this metadata will be ignored if no `issue_url` is set.
///
/// # Examples
///
/// ```rust
/// color_eyre::config::HookBuilder::default()
/// .issue_url("https://github.com/yaahc/jane-eyre/issues/new")
/// .add_issue_metadata("version", "0.1.0")
/// .install()
/// .unwrap();
/// ```
#[cfg(feature = "issue-url")]
#[cfg_attr(docsrs, doc(cfg(feature = "issue-url")))]
pub fn add_issue_metadata<K, V>(mut self, key: K, value: V) -> Self
where
K: Display,
V: Display + Send + Sync + 'static,
{
let pair = (key.to_string(), Box::new(value) as _);
self.issue_metadata.push(pair);
self
}
/// Configures the default capture mode for `SpanTraces` in error reports and panics
pub fn capture_span_trace_by_default(mut self, cond: bool) -> Self {
self.capture_span_trace_by_default = cond;
@ -423,6 +482,8 @@ impl HookBuilder {
}
pub(crate) fn into_hooks(self) -> (PanicHook, EyreHook) {
#[cfg(feature = "issue-url")]
let metadata = Arc::new(self.issue_metadata);
let panic_hook = PanicHook {
filters: self.filters.into_iter().map(Into::into).collect(),
section: self.panic_section,
@ -430,12 +491,20 @@ impl HookBuilder {
capture_span_trace_by_default: self.capture_span_trace_by_default,
display_env_section: self.display_env_section,
panic_message: self.panic_message,
#[cfg(feature = "issue-url")]
issue_url: self.issue_url.clone(),
#[cfg(feature = "issue-url")]
issue_metadata: metadata.clone(),
};
let eyre_hook = EyreHook {
#[cfg(feature = "capture-spantrace")]
capture_span_trace_by_default: self.capture_span_trace_by_default,
display_env_section: self.display_env_section,
#[cfg(feature = "issue-url")]
issue_url: self.issue_url,
#[cfg(feature = "issue-url")]
issue_metadata: metadata,
};
(panic_hook, eyre_hook)
@ -533,6 +602,7 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
hook.panic_message.display(printer.0, out)?;
let v = panic_verbosity();
let capture_bt = v != Verbosity::Minimal;
#[cfg(feature = "capture-spantrace")]
let span_trace = if hook.spantrace_capture_enabled() {
@ -541,6 +611,12 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
None
};
let bt = if capture_bt {
Some(backtrace::Backtrace::new())
} else {
None
};
let mut separated = out.header("\n\n");
if let Some(ref section) = hook.section {
@ -558,10 +634,7 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
}
}
let capture_bt = v != Verbosity::Minimal;
if capture_bt {
let bt = backtrace::Backtrace::new();
if let Some(bt) = bt.as_ref() {
let fmted_bt = hook.format_backtrace(&bt);
write!(
indented(&mut separated.ready()).with_format(Format::Uniform { indentation: " " }),
@ -580,6 +653,27 @@ fn print_panic_info(printer: &PanicPrinter<'_>, out: &mut fmt::Formatter<'_>) ->
write!(&mut separated.ready(), "{}", env_section)?;
}
#[cfg(feature = "issue-url")]
if let Some(url) = &hook.issue_url {
let payload = printer
.0
.payload()
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| printer.0.payload().downcast_ref::<&str>().cloned())
.unwrap_or("<non string panic payload>");
let issue_section = crate::section::github::IssueSection::new(url, payload)
.with_backtrace(bt.as_ref())
.with_location(printer.0.location())
.with_metadata(&**hook.issue_metadata);
#[cfg(feature = "capture-spantrace")]
let issue_section = issue_section.with_span_trace(span_trace.as_ref());
write!(&mut separated.ready(), "{}", issue_section)?;
}
Ok(())
}
@ -590,6 +684,10 @@ pub(crate) struct PanicHook {
#[cfg(feature = "capture-spantrace")]
capture_span_trace_by_default: bool,
display_env_section: bool,
#[cfg(feature = "issue-url")]
issue_url: Option<String>,
#[cfg(feature = "issue-url")]
issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
}
impl PanicHook {
@ -615,6 +713,10 @@ pub(crate) struct EyreHook {
#[cfg(feature = "capture-spantrace")]
capture_span_trace_by_default: bool,
display_env_section: bool,
#[cfg(feature = "issue-url")]
issue_url: Option<String>,
#[cfg(feature = "issue-url")]
issue_metadata: Arc<Vec<(String, Box<dyn Display + Send + Sync + 'static>)>>,
}
impl EyreHook {
@ -641,6 +743,10 @@ impl EyreHook {
span_trace,
sections: Vec::new(),
display_env_section: self.display_env_section,
#[cfg(feature = "issue-url")]
issue_url: self.issue_url.clone(),
#[cfg(feature = "issue-url")]
issue_metadata: self.issue_metadata.clone(),
}
}

View File

@ -11,6 +11,12 @@ use std::fmt::Write;
#[cfg(feature = "capture-spantrace")]
use tracing_error::{ExtractSpanTrace, SpanTrace};
impl std::fmt::Debug for Handler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("redacted")
}
}
impl Handler {
/// Return a reference to the captured `Backtrace` type
pub fn backtrace(&self) -> Option<&Backtrace> {
@ -25,8 +31,6 @@ impl Handler {
}
}
impl Handler {}
impl eyre::EyreHandler for Handler {
fn debug(
&self,
@ -38,14 +42,16 @@ impl eyre::EyreHandler for Handler {
}
#[cfg(feature = "capture-spantrace")]
let errors = eyre::Chain::new(error)
.filter(|e| e.span_trace().is_none())
.enumerate();
let errors = || {
eyre::Chain::new(error)
.filter(|e| e.span_trace().is_none())
.enumerate()
};
#[cfg(not(feature = "capture-spantrace"))]
let errors = eyre::Chain::new(error).enumerate();
let errors = || eyre::Chain::new(error).enumerate();
for (n, error) in errors {
for (n, error) in errors() {
writeln!(f)?;
write!(indented(f).ind(n), "{}", error.bright_red())?;
}
@ -118,6 +124,24 @@ impl eyre::EyreHandler for Handler {
write!(&mut separated.ready(), "{}", env_section)?;
}
#[cfg(feature = "issue-url")]
if let Some(url) = &self.issue_url {
let mut payload = String::from("Error: ");
for (n, error) in errors() {
writeln!(&mut payload)?;
write!(indented(&mut payload).ind(n), "{}", error)?;
}
let issue_section = crate::section::github::IssueSection::new(url, &payload)
.with_backtrace(self.backtrace.as_ref())
.with_metadata(&**self.issue_metadata);
#[cfg(feature = "capture-spantrace")]
let issue_section = issue_section.with_span_trace(span_trace);
write!(&mut separated.ready(), "{}", issue_section)?;
}
Ok(())
}
}

View File

@ -334,7 +334,7 @@
//! [`examples/custom_filter.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_filter.rs
//! [`examples/custom_section.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/custom_section.rs
//! [`examples/multiple_errors.rs`]: https://github.com/yaahc/color-eyre/blob/master/examples/multiple_errors.rs
#![doc(html_root_url = "https://docs.rs/color-eyre/0.5.3")]
#![doc(html_root_url = "https://docs.rs/color-eyre/0.5.4")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(
missing_docs,
@ -359,6 +359,7 @@
while_true
)]
#![allow(clippy::try_err)]
use backtrace::Backtrace;
pub use eyre;
#[doc(hidden)]
@ -393,13 +394,17 @@ mod writers;
/// [`tracing-error`]: https://docs.rs/tracing-error
/// [`color_eyre::Report`]: type.Report.html
/// [`color_eyre::Result`]: type.Result.html
#[derive(Debug)]
pub struct Handler {
backtrace: Option<Backtrace>,
#[cfg(feature = "capture-spantrace")]
span_trace: Option<SpanTrace>,
sections: Vec<HelpInfo>,
display_env_section: bool,
#[cfg(feature = "issue-url")]
issue_url: Option<String>,
#[cfg(feature = "issue-url")]
issue_metadata:
std::sync::Arc<Vec<(String, Box<dyn std::fmt::Display + Send + Sync + 'static>)>>,
}
static CONFIG: OnceCell<config::PanicHook> = OnceCell::new();

185
src/section/github.rs Normal file
View File

@ -0,0 +1,185 @@
use crate::writers::DisplayExt;
use backtrace::Backtrace;
use std::{fmt, panic::Location};
use tracing_error::SpanTrace;
use url::Url;
type Display<'a> = Box<dyn std::fmt::Display + Send + Sync + 'a>;
pub(crate) struct IssueSection<'a> {
url: &'a str,
msg: &'a str,
location: Option<&'a Location<'a>>,
backtrace: Option<&'a Backtrace>,
span_trace: Option<&'a SpanTrace>,
metadata: &'a [(String, Display<'a>)],
}
impl<'a> IssueSection<'a> {
pub(crate) fn new(url: &'a str, msg: &'a str) -> Self {
IssueSection {
url,
msg,
location: None,
backtrace: None,
span_trace: None,
metadata: &[],
}
}
pub(crate) fn with_location(mut self, location: impl Into<Option<&'a Location<'a>>>) -> Self {
self.location = location.into();
self
}
pub(crate) fn with_backtrace(mut self, backtrace: impl Into<Option<&'a Backtrace>>) -> Self {
self.backtrace = backtrace.into();
self
}
pub(crate) fn with_span_trace(mut self, span_trace: impl Into<Option<&'a SpanTrace>>) -> Self {
self.span_trace = span_trace.into();
self
}
pub(crate) fn with_metadata(mut self, metadata: &'a [(String, Display<'a>)]) -> Self {
self.metadata = metadata;
self
}
}
impl fmt::Display for IssueSection<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let location = self
.location
.map(|loc| ("location".to_string(), Box::new(loc) as _));
let metadata = self.metadata.iter().chain(location.as_ref());
let metadata = MetadataSection { metadata }.to_string();
let mut body = Body::new();
body.push_section("Error", ConsoleSection(self.msg))?;
if !self.metadata.is_empty() {
body.push_section("Metadata", metadata)?;
}
if let Some(st) = self.span_trace {
body.push_section(
"SpanTrace",
Collapsed(ConsoleSection(st.with_header("SpanTrace:\n"))),
)?;
}
if let Some(bt) = self.backtrace {
body.push_section(
"Backtrace",
Collapsed(ConsoleSection(
DisplayFromDebug(bt).with_header("Backtrace:\n"),
)),
)?;
}
let url_result = Url::parse_with_params(
self.url,
&[("title", "<autogenerated-issue>"), ("body", &body.body)],
);
let url: &dyn fmt::Display = match &url_result {
Ok(url_struct) => url_struct,
Err(_) => &self.url,
};
url.with_header("Consider reporting this error using this URL: ")
.fmt(f)
}
}
struct Body {
body: String,
}
impl Body {
fn new() -> Self {
Body {
body: String::new(),
}
}
fn push_section<T>(&mut self, header: &'static str, section: T) -> fmt::Result
where
T: fmt::Display,
{
use std::fmt::Write;
let separator = if self.body.is_empty() { "" } else { "\n\n" };
let header = header
.with_header("## ")
.with_header(separator)
.with_footer("\n");
write!(&mut self.body, "{}", section.with_header(header))
}
}
struct MetadataSection<T> {
metadata: T,
}
impl<'a, T> MetadataSection<T>
where
T: IntoIterator<Item = &'a (String, Display<'a>)>,
{
// This is implemented as a free functions so it can consume the `metadata`
// iterator, rather than being forced to leave it unmodified if its behind a
// `&self` shared reference via the Display trait
#[allow(clippy::inherent_to_string, clippy::wrong_self_convention)]
fn to_string(self) -> String {
use std::fmt::Write;
let mut out = String::new();
let f = &mut out;
writeln!(f, "|key|value|").expect("writing to a string doesn't panic");
writeln!(f, "|--|--|").expect("writing to a string doesn't panic");
for (key, value) in self.metadata {
writeln!(f, "|**{}**|{}|", key, value).expect("writing to a string doesn't panic");
}
out
}
}
struct ConsoleSection<T>(T);
impl<T> fmt::Display for ConsoleSection<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(&self.0).with_header("```\n").with_footer("\n```").fmt(f)
}
}
struct Collapsed<T>(T);
impl<T> fmt::Display for Collapsed<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(&self.0)
.with_header("\n<details>\n\n")
.with_footer("\n</details>")
.fmt(f)
}
}
struct DisplayFromDebug<T>(T);
impl<T> fmt::Display for DisplayFromDebug<T>
where
T: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

View File

@ -2,6 +2,8 @@
use crate::writers::WriterExt;
use std::fmt::{self, Display};
#[cfg(feature = "issue-url")]
pub(crate) mod github;
pub(crate) mod help;
/// An indented section with a header for an error report

View File

@ -28,6 +28,27 @@ impl<W> WriterExt for W {
}
}
pub(crate) trait DisplayExt: Sized + Display {
fn with_header<H: Display>(self, header: H) -> Header<Self, H>;
fn with_footer<F: Display>(self, footer: F) -> Footer<Self, F>;
}
impl<T> DisplayExt for T
where
T: Display,
{
fn with_footer<F: Display>(self, footer: F) -> Footer<Self, F> {
Footer { body: self, footer }
}
fn with_header<H: Display>(self, header: H) -> Header<Self, H> {
Header {
body: self,
h: header,
}
}
}
pub(crate) struct ReadyHeaderWriter<'a, 'b, H: ?Sized, W>(&'b mut HeaderWriter<'a, H, W>);
impl<'a, H: ?Sized, W> HeaderWriter<'a, H, W> {
@ -59,6 +80,77 @@ where
}
}
pub(crate) struct FooterWriter<W> {
inner: W,
had_output: bool,
}
impl<W> fmt::Write for FooterWriter<W>
where
W: fmt::Write,
{
fn write_str(&mut self, s: &str) -> fmt::Result {
if !self.had_output && !s.is_empty() {
self.had_output = true;
}
self.inner.write_str(s)
}
}
#[allow(explicit_outlives_requirements)]
pub(crate) struct Footer<B, H>
where
B: Display,
H: Display,
{
body: B,
footer: H,
}
impl<B, H> fmt::Display for Footer<B, H>
where
B: Display,
H: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut inner_f = FooterWriter {
inner: &mut *f,
had_output: false,
};
write!(&mut inner_f, "{}", self.body)?;
if inner_f.had_output {
self.footer.fmt(f)?;
}
Ok(())
}
}
#[allow(explicit_outlives_requirements)]
pub(crate) struct Header<B, H>
where
B: Display,
H: Display,
{
body: B,
h: H,
}
impl<B, H> fmt::Display for Header<B, H>
where
B: Display,
H: Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f.header(&self.h).ready(), "{}", self.body)?;
Ok(())
}
}
#[cfg(feature = "capture-spantrace")]
pub(crate) struct FormattedSpanTrace<'a>(pub(crate) &'a SpanTrace);