Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Verbose Errors and Logs #322

Merged
merged 8 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/notion-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ regex = "1.0.6"
dirs = "1.0.4"
sha-1 = "0.8.1"
hex = "0.3.2"
chrono = "0.4.6"
5 changes: 5 additions & 0 deletions crates/notion-core/src/error/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod details;
mod reporter;

pub use details::ErrorDetails;
pub use reporter::{ErrorContext, ErrorReporter};
120 changes: 120 additions & 0 deletions crates/notion-core/src/error/reporter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use std::env::{self, args_os};
use std::fmt::Write as FmtWrite;
use std::fs::File;
use std::io::Write as IoWrite;

use crate::fs::ensure_containing_dir_exists;
use crate::path::log_dir;
use crate::style::{format_error_cause, format_error_message};
use chrono::Local;
use failure::{Error, Fail};
use notion_fail::NotionError;

const NOTION_DEV: &'static str = "NOTION_DEV";

/// Represents the context from which an error is being reported.
pub enum ErrorContext {
/// An error reported from the `notion` executable.
Notion,

/// An error reported from a shim.
Shim,
}

/// Reporter for showing errors to the terminal and error logs
pub struct ErrorReporter {
/// Notion version to display in error logs
version: String,

/// Flag indicating whether to report additional details to the terminal
verbose: bool,
}

impl ErrorReporter {
/// Create a new ErrorReporter from a verbose flag
pub fn from_flag(notion_version: &str, verbose: bool) -> Self {
if verbose {
ErrorReporter {
version: notion_version.to_string(),
verbose,
}
} else {
ErrorReporter::from_env(notion_version)
}
}

/// Create a new ErrorReporter from the environment variables
pub fn from_env(notion_version: &str) -> Self {
ErrorReporter {
version: notion_version.to_string(),
verbose: env::var(NOTION_DEV).is_ok(),
}
}

/// Report an error, both to the terminal and the error log
pub fn report(&self, cx: ErrorContext, err: &NotionError) {
let message = format_error_message(cx, err);

eprintln!("{}", message);

// Only consider additional details if the error has an underlying cause
if let Some(inner) = err.as_fail().cause() {
let details = compose_error_details(err, inner);

if self.verbose {
eprintln!();
eprintln!("{}", details);
}

match self.write_error_log(message, details) {
Ok(log_file) => {
eprintln!("Error details written to: {}", log_file);
}
Err(_) => {
eprintln!("Unable to write error log!");
}
}
}
}

/// Write an error log with additional details about the error
fn write_error_log(&self, message: String, details: String) -> Result<String, Error> {
let file_name = Local::now()
.format("notion-error-%Y-%m-%d_%H_%M_%S%.3f.log")
.to_string();
let log_file_path = log_dir()?.join(&file_name);

ensure_containing_dir_exists(&log_file_path)?;
let mut log_file = File::create(log_file_path)?;

writeln!(log_file, "{}", collect_arguments())?;
writeln!(log_file, "Notion v{}", self.version)?;
writeln!(log_file)?;
writeln!(log_file, "{}", message)?;
writeln!(log_file)?;
writeln!(log_file, "{}", details)?;

Ok(file_name)
}
}

fn compose_error_details(err: &NotionError, inner: &Fail) -> String {
let mut details = format_error_cause(inner);

// ISSUE #75 - Once we have a way to determine backtraces without RUST_BACKTRACE, we can make this always available
// Until then, we know that if the env var is not set, the backtrace will be empty
if env::var("RUST_BACKTRACE").is_ok() {
// Note: The implementation of `Display` for Backtrace includes a 'stack backtrace:' prefix
write!(details, "\n\n{}", err.backtrace()).expect("write! to a String doesn't fail");
}

details
}

/// Combines all the arguments into a single String
fn collect_arguments() -> String {
args_os()
.map(|arg| arg.into_string().unwrap_or(String::from("<UNKNOWN>")))
charlespierce marked this conversation as resolved.
Show resolved Hide resolved
.collect::<Vec<String>>()
.join(" ")
}
4 changes: 4 additions & 0 deletions crates/notion-core/src/path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ pub fn tmp_dir() -> Fallible<PathBuf> {
Ok(notion_home()?.join("tmp"))
}

pub fn log_dir() -> Fallible<PathBuf> {
Ok(notion_home()?.join("log"))
}

pub fn node_inventory_dir() -> Fallible<PathBuf> {
Ok(inventory_dir()?.join("node"))
}
Expand Down
92 changes: 28 additions & 64 deletions crates/notion-core/src/style.rs
Original file line number Diff line number Diff line change
@@ -1,97 +1,61 @@
//! The view layer of Notion, with utilities for styling command-line output.

use std::env;

use crate::error::ErrorContext;
use archive::Origin;
use console::style;
use console::{style, StyledObject};
use failure::Fail;
use indicatif::{ProgressBar, ProgressStyle};
use notion_fail::NotionError;
use term_size;

const NOTION_DEV: &'static str = "NOTION_DEV";
// ISSUE #306 - When unknown error messages are removed, this can be removed as well
const INTERNAL_ERROR_MESSAGE: &'static str = "an internal error occurred

/// Represents the context from which an error is being reported.
pub enum ErrorContext {
/// An error reported from the `notion` executable.
Notion,
Notion is still a pre-alpha project, so we expect to run into some bugs,
but we'd love to hear about them so we can fix them!

/// An error reported from a shim.
Shim,
}
Please feel free to reach out to us at \x1b[36m\x1b[1m@notionjs\x1b[0m on Twitter or file an issue at:

\x1b[1mhttps://github.com/notion-cli/notion/issues\x1b[0m
";

/// Format an error for output in the given context
pub(crate) fn format_error_message(cx: ErrorContext, err: &NotionError) -> String {
let prefix = error_prefix(cx);

/// Displays an error to stderr.
pub fn display_error(cx: ErrorContext, err: &NotionError) {
display_error_prefix(cx);
if err.is_user_friendly() {
display_user_friendly_error(err);
format!("{} {}", prefix, err)
} else {
display_internal_error(err);
format!("{} {}", prefix, INTERNAL_ERROR_MESSAGE)
}
}

/// Displays a user-friendly error to stderr
fn display_user_friendly_error(err: &NotionError) {
eprintln!("{}", err);

if env::var(NOTION_DEV).is_ok() {
eprintln!();
display_development_details(err);
}
/// Format the underlying cause of an error
pub(crate) fn format_error_cause(inner: &Fail) -> String {
format!(
"{}{} {}",
style("cause").underlined().bold(),
style(":").bold(),
inner
)
}

/// Displays an error to stderr with a styled prefix.
fn display_error_prefix(cx: ErrorContext) {
fn error_prefix(cx: ErrorContext) -> StyledObject<&'static str> {
match cx {
ErrorContext::Notion => {
// Since the command here was `notion`, it would be redundant to say that this was
// a Notion error, so we are less explicit in the heading.
eprint!("{} ", style("error:").red().bold());
style("error:").red().bold()
}
ErrorContext::Shim => {
// Since a Notion error is rare case for a shim, it can be surprising to a user.
// To make it extra clear that this was a failure that happened in Notion when
// attempting to delegate to a shim, we are more explicit about the fact that it's
// a Notion error.
eprint!("{} ", style("Notion error:").red().bold());
style("Notion error:").red().bold()
}
}
}

/// Displays a generic message for internal errors to stderr.
fn display_internal_error(err: &NotionError) {
eprintln!("an internal error occurred");
eprintln!();

if env::var(NOTION_DEV).is_ok() {
display_development_details(err);
} else {
eprintln!("Notion is still a pre-alpha project, so we expect to run into some bugs,");
eprintln!("but we'd love to hear about them so we can fix them!");
eprintln!();
eprintln!(
"Please feel free to reach out to us at {} on Twitter or file an issue at:",
style("@notionjs").cyan().bold()
);
eprintln!();
eprintln!(
" {}",
style("https://github.com/notion-cli/notion/issues").bold()
);
eprintln!();
}
}

fn display_development_details(err: &NotionError) {
eprintln!("{} {:?}", style("details:").yellow().bold(), err);
eprintln!();

// If `RUST_BACKTRACE` is set, then the backtrace will be included in the above output
// If not, we should let the user know how to see the backtrace
if env::var("RUST_BACKTRACE").is_err() {
eprintln!("Run with NOTION_DEV=1 and RUST_BACKTRACE=1 for a backtrace.");
}
}

/// Determines the string to display based on the Origin of the operation.
fn action_str(origin: Origin) -> &'static str {
match origin {
Expand Down
2 changes: 0 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ pub(crate) struct Notion {
#[structopt(subcommand)]
pub(crate) command: Option<Subcommand>,

// not yet implemented!
#[structopt(long = "verbose", help = "Enables verbose diagnostics", global = true)]
#[allow(dead_code)]
pub(crate) verbose: bool,

#[structopt(
Expand Down
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ mod cli;

use structopt::StructOpt;

use notion_core::error::{ErrorContext, ErrorReporter};
use notion_core::session::{ActivityKind, Session};
use notion_core::style::{display_error, ErrorContext};

/// The entry point for the `notion` CLI.
pub fn main() {
Expand All @@ -14,8 +14,10 @@ pub fn main() {
session.add_event_start(ActivityKind::Notion);

let notion = cli::Notion::from_args();
let verbose = notion.verbose;
let exit_code = notion.run(&mut session).unwrap_or_else(|err| {
display_error(ErrorContext::Notion, &err);
ErrorReporter::from_flag(env!("CARGO_PKG_VERSION"), verbose)
.report(ErrorContext::Notion, &err);
session.add_event_error(ActivityKind::Notion, &err);
err.exit_code()
});
Expand Down
4 changes: 2 additions & 2 deletions src/shim.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use notion_core::error::{ErrorContext, ErrorReporter};
use notion_core::session::{ActivityKind, Session};
use notion_core::style::{display_error, ErrorContext};
use notion_core::tool::execute_tool;

use notion_fail::ExitCode;
Expand All @@ -21,7 +21,7 @@ pub fn main() {
session.exit_tool(code);
}
Err(err) => {
display_error(ErrorContext::Shim, &err);
ErrorReporter::from_env(env!("CARGO_PKG_VERSION")).report(ErrorContext::Shim, &err);
session.add_event_error(ActivityKind::Tool, &err);
session.exit(ExitCode::ExecutionFailure);
}
Expand Down
1 change: 1 addition & 0 deletions tests/acceptance/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ mod intercept_global_installs;
mod notion_current;
mod notion_deactivate;
mod notion_pin;
mod verbose_errors;
Loading