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();