diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..9ccf4925 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,64 @@ +//! Error handling facilities + +use std::{fmt, iter}; + +use thiserror::Error; + +/// Wraps any [`Error`][e] type so that [`Display`][d] includes its [sources][s] +/// +/// [e]: std::error::Error +/// [d]: fmt::Display +/// [s]: std::error::Error::source +pub struct Chain<'a>(pub &'a dyn std::error::Error); + +impl fmt::Display for Chain<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0)?; + + let mut source = self.0.source(); + + source + .into_iter() + .chain(iter::from_fn(|| { + source = source.and_then(std::error::Error::source); + source + })) + .try_for_each(|source| write!(f, ": {source}")) + } +} + +/// 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 enum CliError { + #[error( + "the `CONDUIT_CONFIG` environment variable must either be set to a configuration file path \ + or set to an empty string to force configuration through environment variables" + )] + ConfigPathUnset, + + #[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/lib.rs b/src/lib.rs index 5a89f805..6ce754eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod api; pub mod clap; mod config; mod database; +pub mod error; mod service; mod utils; diff --git a/src/main.rs b/src/main.rs index 3d66e5ea..94bd6a51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,25 +48,20 @@ async fn main() -> ExitCode { return ExitCode::SUCCESS; }; - eprintln!("error: {e}"); + eprintln!("error: {}", error::Chain(&e)); ExitCode::FAILURE } /// Fallible entrypoint -async fn try_main() -> Result<(), Box> { +async fn try_main() -> Result<(), error::CliError> { + use error::CliError as Error; + clap::parse(); // Initialize config let raw_config = Figment::new() - .merge( - Toml::file(Env::var("CONDUIT_CONFIG").ok_or( - "the `CONDUIT_CONFIG` environment variable must either be set to a \ - configuration file path or set to the empty string to force configuration \ - through environment variables", - )?) - .nested(), - ) + .merge(Toml::file(Env::var("CONDUIT_CONFIG").ok_or(Error::ConfigPathUnset)?).nested()) .merge(Env::prefixed("CONDUIT_").global()); let config = raw_config.extract::()?; @@ -116,11 +111,13 @@ async fn try_main() -> Result<(), Box> { maximize_fd_limit().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();