From 49660b9e39c6a490d568baef0d3325a33008a46d Mon Sep 17 00:00:00 2001 From: Charles Hall Date: Fri, 19 Apr 2024 15:16:55 -0700 Subject: [PATCH] return a concrete error type from `try_main` Also adds a utility for formatting an error message in addition to all of its sources, i.e. errors that came before it that led to the current error. This helps us to provide better error messages to users by including more information: both our own error message and all of the underlying error messages, if any, instead of only one or the other. --- src/error.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 28 ++++++++++++++------- 2 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 src/error.rs diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..dd50fee0 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,70 @@ +//! Error handling facilities + +use std::{fmt, iter}; + +use thiserror::Error; + +/// Formats an [`Error`][0] and its [`source`][1]s with a separator +/// +/// [0]: std::error::Error +/// [1]: std::error::Error::source +pub(crate) struct DisplayWithSources<'a> { + /// The error (and its sources) to write + pub(crate) error: &'a dyn std::error::Error, + + /// Separator to write between the original error and subsequent sources + pub(crate) infix: &'static str, +} + +impl fmt::Display for DisplayWithSources<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.error)?; + + let mut source = self.error.source(); + + source + .into_iter() + .chain(iter::from_fn(|| { + source = source.and_then(std::error::Error::source); + source + })) + .try_for_each(|source| write!(f, "{}{source}", self.infix)) + } +} + +/// Top-level errors +// Missing docs are allowed here since that kind of information should be +// encoded in the error messages themselves anyway. +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub(crate) enum Main { + #[error( + "the `{0}` environment variable must either be set to a configuration \ + file path or set to an empty string to force configuration through \ + environment variables" + )] + ConfigPathUnset(&'static str), + + #[error("invalid configuration")] + ConfigInvalid(#[from] figment::Error), + + // Upstream's documentation on what this error means is very sparse + #[error("opentelemetry error")] + Otel(#[from] opentelemetry::trace::TraceError), + + #[error("invalid log filter syntax")] + EnvFilter(#[from] tracing_subscriber::filter::ParseError), + + #[error("failed to install global default tracing subscriber")] + SetSubscriber(#[from] tracing::subscriber::SetGlobalDefaultError), + + // Upstream's documentation on what this error means is very sparse + #[error("tracing_flame error")] + TracingFlame(#[from] tracing_flame::Error), + + #[error("failed to load or create the database")] + DatabaseError(#[source] crate::utils::error::Error), + + #[error("failed to serve requests")] + Serve(#[source] std::io::Error), +} diff --git a/src/main.rs b/src/main.rs index c8abc651..b9d165be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,7 @@ pub(crate) mod api; pub(crate) mod clap; mod config; mod database; +mod error; mod service; mod utils; @@ -92,23 +93,30 @@ async fn main() -> ExitCode { return ExitCode::SUCCESS; }; - eprintln!("error: {e}"); + eprintln!( + "Error: {}", + error::DisplayWithSources { + error: &e, + infix: "\n Caused by: " + } + ); ExitCode::FAILURE } /// Fallible entrypoint -async fn try_main() -> Result<(), Box> { +async fn try_main() -> Result<(), error::Main> { + use error::Main as Error; + clap::parse(); // Initialize config let raw_config = Figment::new() .merge( - Toml::file(Env::var("GRAPEVINE_CONFIG").ok_or( - "the `GRAPEVINE_CONFIG` environment variable must either be \ - set to a configuration file path or set to the empty string \ - to force configuration through environment variables", - )?) + Toml::file({ + let name = "GRAPEVINE_CONFIG"; + Env::var(name).ok_or(Error::ConfigPathUnset(name))? + }) .nested(), ) .merge(Env::prefixed("GRAPEVINE_").global()); @@ -164,11 +172,13 @@ async fn try_main() -> Result<(), Box> { .expect("should be able to increase the soft limit to the hard limit"); info!("Loading database"); - KeyValueDatabase::load_or_create(config).await?; + KeyValueDatabase::load_or_create(config) + .await + .map_err(Error::DatabaseError)?; let config = &services().globals.config; info!("Starting server"); - run_server().await?; + run_server().await.map_err(Error::Serve)?; if config.allow_jaeger { opentelemetry::global::shutdown_tracer_provider();