diff --git a/Cargo.toml b/Cargo.toml index 0b96ce5..0868e92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ tokio = { version = "1.36.0", optional = true } # Other axum = "0.8.1" +eyre = { version = "0.6.12", optional = true } serde = { version = "1", features = ["derive"] } thiserror = "2.0.11" tower = "0.5.2" @@ -71,7 +72,7 @@ tokio = { version = "1.43.0", features = ["macros"] } default = ["alloy", "rustls"] alloy = ["dep:alloy"] aws = ["alloy", "alloy?/signer-aws", "dep:async-trait", "dep:aws-config", "dep:aws-sdk-kms"] -perms = ["dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache"] +perms = ["dep:eyre", "dep:oauth2", "dep:tokio", "dep:reqwest", "dep:signet-tx-cache"] pylon = ["perms", "alloy/kzg"] block_watcher = ["dep:tokio"] rustls = ["dep:rustls", "rustls/aws-lc-rs"] diff --git a/examples/ajj.rs b/examples/ajj.rs index 06d2be4..2acdf83 100644 --- a/examples/ajj.rs +++ b/examples/ajj.rs @@ -23,11 +23,29 @@ //! http://localhost:8080/rpc //! ``` use ajj::Router; -use init4_bin_base::init4; +use init4_bin_base::{ + utils::{from_env::FromEnv, metrics::MetricsConfig, tracing::TracingConfig}, + Init4Config, +}; + +#[derive(Debug, FromEnv)] +struct Config { + tracing: TracingConfig, + metrics: MetricsConfig, +} + +impl Init4Config for Config { + fn tracing(&self) -> &TracingConfig { + &self.tracing + } + fn metrics(&self) -> &MetricsConfig { + &self.metrics + } +} #[tokio::main] async fn main() -> Result<(), Box> { - let _guard = init4(); + let _config_and_guard = init4_bin_base::init::()?; let router = Router::<()>::new() .route("helloWorld", || async { diff --git a/examples/build-helper.rs b/examples/build-helper.rs index bed40f1..b0e0067 100644 --- a/examples/build-helper.rs +++ b/examples/build-helper.rs @@ -1,10 +1,29 @@ -use init4_bin_base::init4; +use init4_bin_base::{ + utils::{from_env::FromEnv, metrics::MetricsConfig, tracing::TracingConfig}, + Init4Config, +}; use std::sync::{atomic::AtomicBool, Arc}; -fn main() { +#[derive(Debug, FromEnv)] +struct Config { + tracing: TracingConfig, + metrics: MetricsConfig, +} + +impl Init4Config for Config { + fn tracing(&self) -> &TracingConfig { + &self.tracing + } + fn metrics(&self) -> &MetricsConfig { + &self.metrics + } +} + +fn main() -> eyre::Result<()> { let term: Arc = Default::default(); let _ = signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&term)); - init4(); + let _config_and_guard = init4_bin_base::init::()?; + Ok(()) } diff --git a/examples/otlp-export.rs b/examples/otlp-export.rs index 53ad296..da92957 100644 --- a/examples/otlp-export.rs +++ b/examples/otlp-export.rs @@ -7,22 +7,41 @@ //! //! It can be killed via sigint or sigterm +use eyre::WrapErr; use init4_bin_base::{ deps::tracing::{info, info_span}, - init4, + utils::{from_env::FromEnv, metrics::MetricsConfig, tracing::TracingConfig}, + Init4Config, }; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; +#[derive(Debug, FromEnv)] +struct Config { + tracing: TracingConfig, + metrics: MetricsConfig, +} + +impl Init4Config for Config { + fn tracing(&self) -> &TracingConfig { + &self.tracing + } + fn metrics(&self) -> &MetricsConfig { + &self.metrics + } +} + #[tokio::main] -async fn main() -> Result<(), std::io::Error> { +async fn main() -> eyre::Result<()> { let term: Arc = Default::default(); - signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&term))?; - signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&term))?; + signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&term)) + .wrap_err("failed to register SIGTERM hook")?; + signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&term)) + .wrap_err("failed to register SIGINT hook")?; - let _guard = init4(); + let _config_and_guard = init4_bin_base::init::()?; let mut counter = 0; let _outer = info_span!("outer span").entered(); diff --git a/src/lib.rs b/src/lib.rs index cf1efc0..e58c7e9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,13 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use crate::utils::{ + from_env::{FromEnv, FromEnvErr}, + metrics::MetricsConfig, + otlp::OtelGuard, + tracing::TracingConfig, +}; + #[cfg(feature = "perms")] /// Permissioning and authorization utilities for Signet builders. pub mod perms; @@ -85,7 +92,8 @@ pub mod deps { /// /// [`init_tracing`]: utils::tracing::init_tracing /// [`init_metrics`]: utils::metrics::init_metrics -pub fn init4() -> Option { +#[deprecated(since = "0.18.0-rc.11", note = "use `init` instead")] +pub fn init4() -> Option { let guard = utils::tracing::init_tracing(); utils::metrics::init_metrics(); @@ -96,3 +104,72 @@ pub fn init4() -> Option { guard } + +/// Trait for config types that can be used with [`init`]. +/// +/// Implementors must provide access to [`TracingConfig`] and [`MetricsConfig`], +/// and must be loadable from the environment via [`FromEnv`]. +/// +/// # Example +/// +/// ```ignore +/// #[derive(Debug, FromEnv)] +/// pub struct MyConfig { +/// pub tracing: TracingConfig, +/// pub metrics: MetricsConfig, +/// #[from_env(var = "MY_THING", desc = "some app-specific value")] +/// pub my_thing: String, +/// } +/// +/// impl Init4Config for MyConfig { +/// fn tracing(&self) -> &TracingConfig { &self.tracing } +/// fn metrics(&self) -> &MetricsConfig { &self.metrics } +/// } +/// ``` +pub trait Init4Config: FromEnv { + /// Get the tracing configuration. + fn tracing(&self) -> &TracingConfig; + /// Get the metrics configuration. + fn metrics(&self) -> &MetricsConfig; +} + +/// The result of [`init`]: the loaded config and an optional OTLP guard. +/// +/// The [`OtelGuard`] (if present) must be kept alive for the lifetime of the +/// program to ensure the OTLP exporter continues to send data. +#[derive(Debug)] +pub struct ConfigAndGuard { + /// The loaded configuration. + pub config: T, + /// The OTLP guard, if OTLP was enabled. + pub guard: Option, +} + +/// Load config from the environment and initialize metrics and tracing. +/// +/// This will perform the following: +/// - Load `T` from environment variables via [`FromEnv`] +/// - Read tracing configuration from the loaded config +/// - Determine whether to enable OTLP +/// - Install a global tracing subscriber, using the OTLP provider if enabled +/// - Read metrics configuration from the loaded config +/// - Install a global metrics recorder and serve it over HTTP on 0.0.0.0 +/// +/// See [`init_tracing`] and [`init_metrics`] for more +/// details on specific actions taken and env vars read. +/// +/// [`init_tracing`]: utils::tracing::init_tracing +/// [`init_metrics`]: utils::metrics::init_metrics +pub fn init() -> Result, FromEnvErr> { + let config = T::from_env()?; + + let guard = utils::tracing::init_tracing_with_config(config.tracing().clone()); + utils::metrics::init_metrics_with_config(*config.metrics()); + + // This will install the AWS-LC-Rust TLS provider for rustls, if no other + // provider has been installed yet + #[cfg(feature = "rustls")] + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + Ok(ConfigAndGuard { config, guard }) +} diff --git a/src/perms/oauth.rs b/src/perms/oauth.rs index a372ef9..52c07ab 100644 --- a/src/perms/oauth.rs +++ b/src/perms/oauth.rs @@ -1,7 +1,11 @@ //! Service responsible for authenticating with the cache with Oauth tokens. //! This authenticator periodically fetches a new token every set amount of seconds. -use crate::{deps::tracing::error, utils::from_env::FromEnv}; -use core::{error::Error, fmt}; +use crate::{ + deps::tracing::{debug, warn, Instrument}, + utils::from_env::FromEnv, +}; +use core::fmt; +use eyre::eyre; use oauth2::{ basic::{BasicClient, BasicTokenType}, AccessToken, AuthUrl, ClientId, ClientSecret, EmptyExtraTokenFields, EndpointNotSet, @@ -12,8 +16,8 @@ use std::{future::IntoFuture, pin::Pin}; use tokio::{ sync::watch::{self, Ref}, task::JoinHandle, + time::MissedTickBehavior, }; -use tracing::{debug, Instrument}; type Token = StandardTokenResponse; @@ -88,10 +92,16 @@ impl Authenticator { .set_auth_uri(AuthUrl::from_url(config.oauth_authenticate_url.clone())) .set_token_uri(TokenUrl::from_url(config.oauth_token_url.clone())); - // NB: this is MANDATORY + // NB: redirect policy none is MANDATORY // https://docs.rs/oauth2/latest/oauth2/#security-warning + // + // Disable connection pooling to avoid stale connection errors. + // OAuth refreshes are infrequent (typically every 60s), so idle + // connections are almost always closed by the server or + // intermediary (e.g. Istio envoy) before the next request. let rq_client = reqwest::Client::builder() .redirect(reqwest::redirect::Policy::none()) + .pool_max_idle_per_host(0) .build() .unwrap(); @@ -159,31 +169,20 @@ impl Authenticator { /// Create a future that contains the periodic refresh loop. async fn task_future(self) { - let interval = self.config.oauth_token_refresh_interval; + let duration = tokio::time::Duration::from_secs(self.config.oauth_token_refresh_interval); + let mut interval = tokio::time::interval(duration); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); loop { + interval.tick().await; debug!("Refreshing oauth token"); match self.authenticate().await { - Ok(_) => { - debug!("Successfully refreshed oauth token"); - } - Err(err) => { - let mut current = &err as &dyn Error; - - // This is a little hacky, but the oauth library nests - // errors quite deeply, so we need to walk the source chain - // to get the full picture. - let mut source_chain = Vec::new(); - while let Some(source) = current.source() { - source_chain.push(source.to_string()); - current = source; - } - let source_chain = source_chain.join("\n\n Caused by: \n"); - - error!(%err, %source_chain, "Failed to refresh oauth token"); - } + Ok(_) => debug!("Successfully refreshed oauth token"), + Err(error) => warn!( + error = %format!("{:#}", eyre!(error)), + "Failed to refresh oauth token" + ), }; - let _sleep = tokio::time::sleep(tokio::time::Duration::from_secs(interval)).await; } } diff --git a/src/utils/from_env.rs b/src/utils/from_env.rs index 1bf51ea..7a9ad97 100644 --- a/src/utils/from_env.rs +++ b/src/utils/from_env.rs @@ -1,8 +1,12 @@ +use core::{ + fmt::{self, Display, Formatter}, + str::FromStr, +}; use signet_constants::{ HostConstants, RollupConstants, SignetConstants, SignetEnvironmentConstants, SignetSystemConstants, }; -use std::{env::VarError, str::FromStr}; +use std::env::VarError; use tracing_core::metadata::ParseLevelError; use crate::utils::calc::SlotCalculator; @@ -249,7 +253,7 @@ where /// ensure that env-related errors (e.g. missing env vars) are not lost in the /// recursive structure of parsing errors. Environment errors are always at the /// top level, and should never be nested. -pub trait FromEnv: core::fmt::Debug + Sized + 'static { +pub trait FromEnv: fmt::Debug + Sized + 'static { /// Get the required environment variable names for this type. /// /// ## Note @@ -266,8 +270,10 @@ pub trait FromEnv: core::fmt::Debug + Sized + 'static { fn check_inventory() -> Result<(), Vec<&'static EnvItemInfo>> { let mut missing = Vec::new(); for var in Self::inventory() { - if std::env::var(var.var).is_err() && !var.optional { - missing.push(var); + match std::env::var(var.var) { + Ok(s) if s.is_empty() && !var.optional => missing.push(var), + Err(VarError::NotPresent) if !var.optional => missing.push(var), + _ => {} } } if missing.is_empty() { @@ -454,7 +460,8 @@ where match std::env::var(env_var) { Ok(s) if s.is_empty() => Ok(None), Ok(_) => T::from_env_var(env_var).map(Some), - Err(_) => Ok(None), + Err(VarError::NotPresent) => Ok(None), + Err(error) => Err(FromEnvErr::parse_error(env_var, error)), } } } @@ -515,6 +522,118 @@ where } } +/// Generate an `Optional{Name}WithDefault` newtype that wraps `$inner` with a const generic +/// default. When the env var is missing or empty, the default is used. The `$parse` closure +/// controls how a non-empty string is converted to the inner type. +macro_rules! optional_with_default { + ( + $(#[$meta:meta])* + $name:ident, $inner:ty, |$var:ident, $s:ident| $parse:expr + ) => { + $(#[$meta])* + #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Debug)] + pub struct $name($inner); + + impl $name { + /// Extract the inner value. + pub const fn into_inner(self) -> $inner { + self.0 + } + } + + impl Display for $name { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + <$inner as Display>::fmt(&self.0, f) + } + } + + impl Default for $name { + fn default() -> Self { + Self(DEFAULT) + } + } + + impl FromEnvVar for $name { + fn from_env_var($var: &str) -> Result { + match std::env::var($var) { + Ok($s) if $s.is_empty() => Ok(Self(DEFAULT)), + Ok($s) => $parse.map(Self), + Err(VarError::NotPresent) => Ok(Self(DEFAULT)), + Err(error) => Err(FromEnvErr::parse_error($var, error)), + } + } + } + }; +} + +optional_with_default! { + /// An optional boolean with a const default, for use in config structs. + /// A non-empty value is treated as `true`; missing or empty falls back to the default. + OptionalBoolWithDefault, bool, |env_var, _s| Ok(true) +} + +/// Helper for numeric `optional_with_default!` invocations: parse a non-empty string. +fn parse_or_err(env_var: &str, s: &str) -> Result +where + T::Err: core::error::Error + Send + Sync + 'static, +{ + s.parse::() + .map_err(|error| FromEnvErr::parse_error(env_var, error)) +} + +optional_with_default! { + /// An optional `u8` with a const default, for use in config structs. + OptionalU8WithDefault, u8, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `u16` with a const default, for use in config structs. + OptionalU16WithDefault, u16, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `u32` with a const default, for use in config structs. + OptionalU32WithDefault, u32, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `u64` with a const default, for use in config structs. + OptionalU64WithDefault, u64, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `u128` with a const default, for use in config structs. + OptionalU128WithDefault, u128, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `usize` with a const default, for use in config structs. + OptionalUsizeWithDefault, usize, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `i8` with a const default, for use in config structs. + OptionalI8WithDefault, i8, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `i16` with a const default, for use in config structs. + OptionalI16WithDefault, i16, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `i32` with a const default, for use in config structs. + OptionalI32WithDefault, i32, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `i64` with a const default, for use in config structs. + OptionalI64WithDefault, i64, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `i128` with a const default, for use in config structs. + OptionalI128WithDefault, i128, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `isize` with a const default, for use in config structs. + OptionalIsizeWithDefault, isize, |env_var, s| parse_or_err(env_var, &s) +} +optional_with_default! { + /// An optional `char` with a const default, for use in config structs. + OptionalCharWithDefault, char, |env_var, s| parse_or_err(env_var, &s) +} + macro_rules! impl_for_parseable { ($($t:ty),*) => { $( @@ -594,9 +713,8 @@ impl FromEnv for SignetSystemConstants { fn inventory() -> Vec<&'static EnvItemInfo> { vec![&EnvItemInfo { var: "CHAIN_NAME", - description: - "The name of the chain. If set, the other environment variables are ignored.", - optional: true, + description: "The name of the chain", + optional: false, }] } diff --git a/src/utils/metrics.rs b/src/utils/metrics.rs index 8754220..66bff64 100644 --- a/src/utils/metrics.rs +++ b/src/utils/metrics.rs @@ -72,8 +72,11 @@ impl FromEnv for MetricsConfig { /// is in use. pub fn init_metrics() { let cfg = MetricsConfig::from_env().unwrap(); + init_metrics_with_config(cfg) +} - let address = SocketAddr::from(([0, 0, 0, 0], cfg.port)); +pub(crate) fn init_metrics_with_config(config: MetricsConfig) { + let address = SocketAddr::from(([0, 0, 0, 0], config.port)); PrometheusBuilder::new() .with_http_listener(address) diff --git a/src/utils/tracing.rs b/src/utils/tracing.rs index a3e9505..5252600 100644 --- a/src/utils/tracing.rs +++ b/src/utils/tracing.rs @@ -1,29 +1,64 @@ use crate::utils::{ - from_env::FromEnvVar, + from_env::{FromEnv, OptionalBoolWithDefault}, otlp::{OtelConfig, OtelGuard}, }; use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, util::SubscriberInitExt, Layer}; -const TRACING_LOG_JSON: &str = "TRACING_LOG_JSON"; - -/// Install a format layer based on the `TRACING_LOG_JSON` environment -/// variable, and then install the registr +/// Tracing format configuration. /// +/// Uses the following environment variables: +/// - `TRACING_LOG_JSON` - optional. If `true`, enables JSON logging. Defaults to `false`. +/// - `TRACING_WITH_FILE_AND_LINE_NO` - optional. If `true`, includes file names and line numbers +/// in tracing output. Defaults to `false`. +#[derive(Debug, Clone, Default, FromEnv)] +#[non_exhaustive] +#[from_env(crate)] +pub struct TracingConfig { + /// Whether to log in JSON or not. + #[from_env( + var = "TRACING_LOG_JSON", + desc = "If non-empty, log in JSON format [default: disabled]", + optional + )] + pub log_json: OptionalBoolWithDefault, + + /// Whether to include file names and line numbers in log output. + #[from_env( + var = "TRACING_WITH_FILE_AND_LINE_NO", + desc = "If non-empty, include file names and line numbers in tracing output [default: disabled]", + optional + )] + pub with_file_and_line_number: OptionalBoolWithDefault, + + /// OTEL configuration. + pub otel_config: Option, +} + +/// Install a format layer and the registry. macro_rules! install_fmt { - (json @ $registry:ident, $filter:ident) => {{ - let fmt = tracing_subscriber::fmt::layer().json().with_span_list(true).with_filter($filter); + (json @ $registry:ident, $filter:ident, $file_line:expr) => {{ + let fmt = tracing_subscriber::fmt::layer() + .json() + .with_span_list(true) + .with_current_span(false) + .with_file($file_line) + .with_line_number($file_line) + .with_filter($filter); $registry.with(fmt).init(); }}; - (log @ $registry:ident, $filter:ident) => {{ - let fmt = tracing_subscriber::fmt::layer().with_filter($filter); + (log @ $registry:ident, $filter:ident, $file_line:expr) => {{ + let fmt = tracing_subscriber::fmt::layer() + .with_file($file_line) + .with_line_number($file_line) + .with_filter($filter); $registry.with(fmt).init(); }}; - ($registry:ident, $filter:ident) => {{ - let json = bool::from_env_var(TRACING_LOG_JSON).unwrap_or(false); - if json { - install_fmt!(json @ $registry, $filter); + ($registry:ident, $filter:ident, $cfg:expr) => {{ + let file_line = $cfg.with_file_and_line_number.into_inner(); + if $cfg.log_json.into_inner() { + install_fmt!(json @ $registry, $filter, file_line); } else { - install_fmt!(log @ $registry, $filter); + install_fmt!(log @ $registry, $filter, file_line); } }}; } @@ -35,10 +70,7 @@ macro_rules! install_fmt { /// environment variables are set, it will initialize the OTEL provider /// with the specified configuration, as well as the `fmt` layer. /// -/// ## Env Reads -/// -/// - `TRACING_LOG_JSON` - If set, will enable JSON logging. -/// - As [`OtelConfig`] documentation for env var information. +/// See [`TracingConfig`] and [`OtelConfig`] for env var information. /// /// ## Panics /// @@ -46,16 +78,21 @@ macro_rules! install_fmt { /// /// [`OtelConfig`]: crate::utils::otlp::OtelConfig pub fn init_tracing() -> Option { + let tracing_config = TracingConfig::from_env().unwrap(); + init_tracing_with_config(tracing_config) +} + +pub(crate) fn init_tracing_with_config(tracing_config: TracingConfig) -> Option { let registry = tracing_subscriber::registry(); let filter = EnvFilter::from_default_env(); - if let Some(cfg) = OtelConfig::load() { - let (guard, layer) = cfg.into_guard_and_layer(); + if let Some(otel_config) = tracing_config.otel_config { + let (guard, layer) = otel_config.into_guard_and_layer(); let registry = registry.with(layer); - install_fmt!(registry, filter); + install_fmt!(registry, filter, tracing_config); Some(guard) } else { - install_fmt!(registry, filter); + install_fmt!(registry, filter, tracing_config); tracing::debug!( "No OTEL config found or error while loading otel config, using default tracing" );