diff --git a/Cargo.lock b/Cargo.lock index ccb5359c..31d5857b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,7 +1548,11 @@ dependencies = [ name = "base-cli-utils" version = "0.2.1" dependencies = [ + "clap", + "eyre", "reth", + "tokio", + "tracing", ] [[package]] diff --git a/crates/shared/cli-utils/Cargo.toml b/crates/shared/cli-utils/Cargo.toml index 99c061b2..d477a71b 100644 --- a/crates/shared/cli-utils/Cargo.toml +++ b/crates/shared/cli-utils/Cargo.toml @@ -13,3 +13,7 @@ workspace = true [dependencies] reth.workspace = true +tokio = { workspace = true, features = ["full"] } +eyre.workspace = true +clap = { workspace = true, features = ["derive"] } +tracing.workspace = true diff --git a/crates/shared/cli-utils/README.md b/crates/shared/cli-utils/README.md index c3d1a7b3..3252be4e 100644 --- a/crates/shared/cli-utils/README.md +++ b/crates/shared/cli-utils/README.md @@ -1,35 +1,44 @@ -# `base-cli` +# `base-cli-utils` CI MIT License -CLI utilities for the Base Reth node. Provides versioning and client identification for the Base node binary. +CLI utilities for the Base Reth node. ## Overview -- **`Version`**: Handles versioning metadata for the Base Reth node, including client version strings and P2P identification. +- **`Version`**: Client versioning and P2P identification. +- **`GlobalArgs`**: Common CLI arguments (chain ID, logging). +- **`LoggingArgs`**: Verbosity levels, output formats, file rotation. +- **`runtime`**: Tokio runtime with Ctrl+C shutdown handling. ## Usage -Add the dependency to your `Cargo.toml`: - ```toml [dependencies] -base-cli = { git = "https://github.com/base/node-reth" } +base-cli-utils = { git = "https://github.com/base/node-reth" } ``` -Initialize versioning at node startup: - ```rust,ignore -use base_cli::Version; - -// Initialize version metadata before starting the node -Version::init(); - -// Access the client version string -let client_version = Version::NODE_RETH_CLIENT_VERSION; +use base_cli_utils::{GlobalArgs, Version}; +use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c}; +use clap::Parser; + +#[derive(Parser)] +struct MyCli { + #[command(flatten)] + global: GlobalArgs, +} + +fn main() -> eyre::Result<()> { + Version::init(); + let cli = MyCli::parse(); + + let runtime = build_runtime()?; + runtime.block_on(run_until_ctrl_c(async { /* ... */ })) +} ``` ## License -Licensed under the [MIT License](https://github.com/base/node-reth/blob/main/LICENSE). +[MIT License](https://github.com/base/node-reth/blob/main/LICENSE) diff --git a/crates/shared/cli-utils/src/args.rs b/crates/shared/cli-utils/src/args.rs new file mode 100644 index 00000000..de4895f1 --- /dev/null +++ b/crates/shared/cli-utils/src/args.rs @@ -0,0 +1,17 @@ +//! Global CLI arguments for the Base Reth node. + +use super::LoggingArgs; + +/// Global arguments shared across all CLI commands. +/// +/// Chain ID defaults to Base Mainnet (8453). Can be set via `--chain-id` or `BASE_NETWORK` env. +#[derive(Debug, Clone, clap::Args)] +pub struct GlobalArgs { + /// Chain ID (8453 = Base Mainnet, 84532 = Base Sepolia). + #[arg(long = "chain-id", env = "BASE_NETWORK", default_value = "8453")] + pub chain_id: u64, + + /// Logging configuration. + #[command(flatten)] + pub logging: LoggingArgs, +} diff --git a/crates/shared/cli-utils/src/lib.rs b/crates/shared/cli-utils/src/lib.rs index 0f1115dd..b8eed9da 100644 --- a/crates/shared/cli-utils/src/lib.rs +++ b/crates/shared/cli-utils/src/lib.rs @@ -3,5 +3,13 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr(not(test), warn(unused_crate_dependencies))] +mod args; +mod logging; mod version; + +pub use args::GlobalArgs; +pub use logging::{LogFormat, LogRotation, LoggingArgs}; pub use version::Version; + +pub mod runtime; +pub use runtime::{build_runtime, run_until_ctrl_c, run_until_ctrl_c_fallible}; diff --git a/crates/shared/cli-utils/src/logging.rs b/crates/shared/cli-utils/src/logging.rs new file mode 100644 index 00000000..0ab096e1 --- /dev/null +++ b/crates/shared/cli-utils/src/logging.rs @@ -0,0 +1,136 @@ +//! Logging configuration for CLI applications. + +use std::path::PathBuf; + +use clap::{ArgAction, Parser, ValueEnum}; + +/// Log output format. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] +pub enum LogFormat { + /// Full format with timestamp, level, target, and spans. + #[default] + Full, + /// Compact format with minimal metadata. + Compact, + /// JSON format for structured logging. + Json, +} + +/// Log rotation strategy. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] +pub enum LogRotation { + /// Rotate every minute (for testing). + Minutely, + /// Rotate every hour. + #[default] + Hourly, + /// Rotate every day. + Daily, + /// Never rotate. + Never, +} + +/// Logging configuration arguments. +/// +/// Verbosity: `-v` (INFO), `-vv` (DEBUG), `-vvv` (TRACE). Default is WARN. +#[derive(Debug, Clone, Default, PartialEq, Eq, Parser)] +pub struct LoggingArgs { + /// Increase logging verbosity (-v, -vv, -vvv). + #[arg(short = 'v', long = "verbose", action = ArgAction::Count, global = true)] + pub verbosity: u8, + + /// Log output format (full, compact, json). + #[arg(long = "log-format", default_value = "full", global = true)] + pub format: LogFormat, + + /// Path to write logs to a file. + #[arg(long = "log-file", global = true)] + pub log_file: Option, + + /// Log file rotation (minutely, hourly, daily, never). + #[arg(long = "log-rotation", global = true)] + pub log_rotation: Option, +} + +impl LoggingArgs { + /// Converts verbosity to a [`tracing::Level`]. + #[inline] + pub const fn log_level(&self) -> tracing::Level { + match self.verbosity { + 0 => tracing::Level::WARN, + 1 => tracing::Level::INFO, + 2 => tracing::Level::DEBUG, + _ => tracing::Level::TRACE, + } + } + + /// Converts verbosity to a [`tracing::level_filters::LevelFilter`]. + #[inline] + pub const fn log_level_filter(&self) -> tracing::level_filters::LevelFilter { + match self.verbosity { + 0 => tracing::level_filters::LevelFilter::WARN, + 1 => tracing::level_filters::LevelFilter::INFO, + 2 => tracing::level_filters::LevelFilter::DEBUG, + _ => tracing::level_filters::LevelFilter::TRACE, + } + } + + /// Returns `true` if file logging is enabled. + #[inline] + pub const fn has_file_logging(&self) -> bool { + self.log_file.is_some() + } + + /// Returns the log rotation strategy, defaulting to [`LogRotation::Hourly`]. + #[inline] + pub fn rotation(&self) -> LogRotation { + self.log_rotation.unwrap_or_default() + } + + /// Returns `true` if the log format is JSON. + #[inline] + pub const fn is_json_format(&self) -> bool { + matches!(self.format, LogFormat::Json) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_verbosity_levels() { + assert_eq!(LoggingArgs::default().log_level(), tracing::Level::WARN); + assert_eq!( + LoggingArgs { verbosity: 1, ..Default::default() }.log_level(), + tracing::Level::INFO + ); + assert_eq!( + LoggingArgs { verbosity: 2, ..Default::default() }.log_level(), + tracing::Level::DEBUG + ); + assert_eq!( + LoggingArgs { verbosity: 3, ..Default::default() }.log_level(), + tracing::Level::TRACE + ); + } + + #[test] + fn test_format_and_rotation() { + let args = LoggingArgs::default(); + assert_eq!(args.format, LogFormat::Full); + assert!(!args.is_json_format()); + assert_eq!(args.rotation(), LogRotation::Hourly); + + let args = LoggingArgs { format: LogFormat::Json, ..Default::default() }; + assert!(args.is_json_format()); + } + + #[test] + fn test_file_logging() { + assert!(!LoggingArgs::default().has_file_logging()); + let args = + LoggingArgs { log_file: Some(PathBuf::from("/tmp/test.log")), ..Default::default() }; + assert!(args.has_file_logging()); + } +} diff --git a/crates/shared/cli-utils/src/runtime.rs b/crates/shared/cli-utils/src/runtime.rs new file mode 100644 index 00000000..ac2b61e4 --- /dev/null +++ b/crates/shared/cli-utils/src/runtime.rs @@ -0,0 +1,43 @@ +//! Tokio runtime utilities with graceful Ctrl+C shutdown handling. + +use std::future::Future; + +/// Builds a multi-threaded Tokio runtime with all features enabled. +pub fn build_runtime() -> eyre::Result { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .map_err(|e| eyre::eyre!("Failed to build tokio runtime: {}", e)) +} + +/// Runs a future to completion, returning early on Ctrl+C. +pub async fn run_until_ctrl_c(fut: F) -> eyre::Result<()> +where + F: Future, +{ + let ctrl_c = async { + tokio::signal::ctrl_c().await.expect("Failed to install Ctrl+C handler"); + }; + + tokio::select! { + biased; + () = ctrl_c => Ok(()), + () = fut => Ok(()), + } +} + +/// Runs a fallible future to completion, returning early on Ctrl+C. +pub async fn run_until_ctrl_c_fallible(fut: F) -> eyre::Result<()> +where + F: Future>, +{ + let ctrl_c = async { + tokio::signal::ctrl_c().await.expect("Failed to install Ctrl+C handler"); + }; + + tokio::select! { + biased; + () = ctrl_c => Ok(()), + result = fut => result, + } +}