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 5fd248a5..d2df53ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{future::Future, io, net::SocketAddr, sync::atomic, time::Duration}; +use std::{future::Future, io, net::SocketAddr, process::ExitCode, sync::atomic, time::Duration}; use axum::{ extract::{DefaultBodyLimit, FromRequestParts, MatchedPath}, @@ -31,7 +31,7 @@ use tower_http::{ trace::TraceLayer, ServiceBuilderExt as _, }; -use tracing::{debug, error, info, warn}; +use tracing::{debug, info, warn}; use tracing_subscriber::{prelude::*, EnvFilter}; pub use conduit::*; // Re-export everything from the library crate @@ -44,27 +44,28 @@ use tikv_jemallocator::Jemalloc; static GLOBAL: Jemalloc = Jemalloc; #[tokio::main] -async fn main() { +async fn main() -> ExitCode { + let Err(e) = try_main().await else { + return ExitCode::SUCCESS; + }; + + eprintln!("error: {}", error::Chain(&e)); + + ExitCode::FAILURE +} + +/// Fallible entrypoint +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").expect( - "The CONDUIT_CONFIG env var needs to be set. Example: /etc/conduit.toml", - )) - .nested(), - ) - .merge(Env::prefixed("CONDUIT_").global()); + let raw_config = Figment::new() + .merge(Toml::file(Env::var("CONDUIT_CONFIG").ok_or(Error::ConfigPathUnset)?).nested()) + .merge(Env::prefixed("CONDUIT_").global()); - let config = match raw_config.extract::() { - Ok(s) => s, - Err(e) => { - eprintln!("It looks like your config is invalid. The following error occurred: {e}"); - std::process::exit(1); - } - }; + let config = raw_config.extract::()?; config.warn_deprecated(); @@ -73,47 +74,31 @@ async fn main() { let tracer = opentelemetry_jaeger::new_agent_pipeline() .with_auto_split_batch(true) .with_service_name("conduit") - .install_batch(opentelemetry::runtime::Tokio) - .unwrap(); + .install_batch(opentelemetry::runtime::Tokio)?; let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - let filter_layer = match EnvFilter::try_new(&config.log) { - Ok(s) => s, - Err(e) => { - eprintln!( - "It looks like your log config is invalid. The following error occurred: {e}" - ); - EnvFilter::try_new("warn").unwrap() - } - }; + let filter_layer = EnvFilter::try_new(&config.log)?; let subscriber = tracing_subscriber::Registry::default() .with(filter_layer) .with(telemetry); - tracing::subscriber::set_global_default(subscriber).unwrap(); + tracing::subscriber::set_global_default(subscriber)?; } else if config.tracing_flame { let registry = tracing_subscriber::Registry::default(); - let (flame_layer, _guard) = - tracing_flame::FlameLayer::with_file("./tracing.folded").unwrap(); + let (flame_layer, _guard) = tracing_flame::FlameLayer::with_file("./tracing.folded")?; let flame_layer = flame_layer.with_empty_samples(false); let filter_layer = EnvFilter::new("trace,h2=off"); let subscriber = registry.with(filter_layer).with(flame_layer); - tracing::subscriber::set_global_default(subscriber).unwrap(); + tracing::subscriber::set_global_default(subscriber)?; } else { let registry = tracing_subscriber::Registry::default(); let fmt_layer = tracing_subscriber::fmt::Layer::new(); - let filter_layer = match EnvFilter::try_new(&config.log) { - Ok(s) => s, - Err(e) => { - eprintln!("It looks like your config is invalid. The following error occured while parsing it: {e}"); - EnvFilter::try_new("warn").unwrap() - } - }; + let filter_layer = EnvFilter::try_new(&config.log)?; let subscriber = registry.with(filter_layer).with(fmt_layer); - tracing::subscriber::set_global_default(subscriber).unwrap(); + tracing::subscriber::set_global_default(subscriber)?; } // This is needed for opening lots of file descriptors, which tends to @@ -127,19 +112,19 @@ async fn main() { maximize_fd_limit().expect("should be able to increase the soft limit to the hard limit"); info!("Loading database"); - if let Err(error) = KeyValueDatabase::load_or_create(config).await { - error!(?error, "The database couldn't be loaded or created"); - - std::process::exit(1); - }; + KeyValueDatabase::load_or_create(config) + .await + .map_err(Error::DatabaseError)?; let config = &services().globals.config; info!("Starting server"); - run_server().await.unwrap(); + run_server().await.map_err(Error::Serve)?; if config.allow_jaeger { opentelemetry::global::shutdown_tracer_provider(); } + + Ok(()) } /// Adds additional headers to prevent any potential XSS attacks via the media repo