From 415699f57f7ec37b19f1bb66f1e3cf1a41dddfc2 Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Fri, 9 Jan 2026 18:07:34 -0500 Subject: [PATCH 1/3] feat(cli): add some basic utility methods --- Cargo.lock | 4 + crates/shared/cli-utils/Cargo.toml | 4 + crates/shared/cli-utils/README.md | 121 ++++++++- crates/shared/cli-utils/src/args.rs | 59 +++++ crates/shared/cli-utils/src/lib.rs | 8 + crates/shared/cli-utils/src/logging.rs | 332 +++++++++++++++++++++++++ crates/shared/cli-utils/src/runtime.rs | 141 +++++++++++ 7 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 crates/shared/cli-utils/src/args.rs create mode 100644 crates/shared/cli-utils/src/logging.rs create mode 100644 crates/shared/cli-utils/src/runtime.rs 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..81d827b0 100644 --- a/crates/shared/cli-utils/README.md +++ b/crates/shared/cli-utils/README.md @@ -1,13 +1,16 @@ -# `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. Provides shared infrastructure for versioning, argument parsing, logging configuration, and runtime management. ## Overview - **`Version`**: Handles versioning metadata for the Base Reth node, including client version strings and P2P identification. +- **`GlobalArgs`**: Common CLI arguments with network chain ID and logging configuration. +- **`LoggingArgs`**: Structured logging configuration with verbosity levels, output formats, and file rotation. +- **`runtime`**: Tokio runtime utilities with graceful Ctrl+C shutdown handling. ## Usage @@ -15,13 +18,15 @@ 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" } ``` +### Version Initialization + Initialize versioning at node startup: ```rust,ignore -use base_cli::Version; +use base_cli_utils::Version; // Initialize version metadata before starting the node Version::init(); @@ -30,6 +35,114 @@ Version::init(); let client_version = Version::NODE_RETH_CLIENT_VERSION; ``` +### Global Arguments + +Use `GlobalArgs` for common CLI configuration: + +```rust,ignore +use base_cli_utils::GlobalArgs; +use clap::Parser; + +#[derive(Parser)] +struct MyCli { + #[command(flatten)] + global: GlobalArgs, +} + +let cli = MyCli::parse(); +println!("Chain ID: {}", cli.global.chain_id); +println!("Log level: {:?}", cli.global.logging.log_level()); +``` + +Command line usage: + +```bash +# Default (Base Mainnet, WARN level) +my-app + +# Base Sepolia with INFO logging +my-app --chain-id 84532 -v + +# Using environment variable +BASE_NETWORK=84532 my-app -vv +``` + +### Logging Configuration + +Use `LoggingArgs` for standalone logging configuration: + +```rust,ignore +use base_cli_utils::LoggingArgs; +use clap::Parser; + +#[derive(Parser)] +struct MyCli { + #[command(flatten)] + logging: LoggingArgs, +} + +let cli = MyCli::parse(); +let level = cli.logging.log_level(); +let filter = cli.logging.log_level_filter(); +``` + +Verbosity levels: +- No flag: `WARN` level +- `-v`: `INFO` level +- `-vv`: `DEBUG` level +- `-vvv`: `TRACE` level + +Output formats: +- `--log-format full`: Complete format with timestamp, level, target, and spans +- `--log-format compact`: Minimal format with just level and message +- `--log-format json`: Structured JSON format for log aggregation + +File logging with rotation: + +```bash +my-app -vv --log-file /var/log/app.log --log-rotation daily +``` + +### Runtime Utilities + +Build and run a Tokio runtime with graceful shutdown: + +```rust,ignore +use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c}; + +async fn my_long_running_task() { + loop { + // Do some work + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } +} + +fn main() -> eyre::Result<()> { + let runtime = build_runtime()?; + runtime.block_on(async { + run_until_ctrl_c(my_long_running_task()).await + }) +} +``` + +For fallible tasks: + +```rust,ignore +use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c_fallible}; + +async fn my_fallible_task() -> eyre::Result<()> { + // Do some work that might fail + Ok(()) +} + +fn main() -> eyre::Result<()> { + let runtime = build_runtime()?; + runtime.block_on(async { + run_until_ctrl_c_fallible(my_fallible_task()).await + }) +} +``` + ## License Licensed under the [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..8a82ea27 --- /dev/null +++ b/crates/shared/cli-utils/src/args.rs @@ -0,0 +1,59 @@ +//! Global CLI arguments for the Base Reth node. +//! +//! This module provides common command-line arguments that are shared across +//! different CLI commands and subcommands. +//! +//! # Example +//! +//! ```ignore +//! use base_cli_utils::GlobalArgs; +//! use clap::Parser; +//! +//! #[derive(Debug, Parser)] +//! struct MyCli { +//! #[command(flatten)] +//! global: GlobalArgs, +//! } +//! ``` + +use super::LoggingArgs; + +/// Global arguments shared across all CLI commands. +/// +/// These arguments provide common configuration options that apply to the entire +/// application, such as network selection and logging configuration. +/// +/// # Network Configuration +/// +/// The chain ID can be configured via the `--chain-id` flag or the `BASE_NETWORK` +/// environment variable. If neither is specified, it defaults to Base Mainnet (8453). +/// +/// # Example +/// +/// ```ignore +/// // Using default Base Mainnet +/// $ my-cli +/// +/// // Using Base Sepolia testnet +/// $ my-cli --chain-id 84532 +/// +/// // Using environment variable +/// $ BASE_NETWORK=84532 my-cli +/// ``` +#[derive(Debug, Clone, clap::Args)] +pub struct GlobalArgs { + /// The chain ID of the network to connect to. + /// + /// Defaults to Base Mainnet (8453). Can also be set via the `BASE_NETWORK` + /// environment variable. + /// + /// Common values: + /// - 8453: Base Mainnet + /// - 84532: Base Sepolia + #[arg(long = "chain-id", env = "BASE_NETWORK", default_value = "8453")] + pub chain_id: u64, + + /// Logging configuration arguments. + #[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..7f404c3e --- /dev/null +++ b/crates/shared/cli-utils/src/logging.rs @@ -0,0 +1,332 @@ +//! Logging configuration utilities for CLI applications. +//! +//! This module provides structured logging configuration with support for: +//! - Verbosity levels (`-v`, `-vv`, `-vvv`) +//! - Multiple output formats (full, compact, json) +//! - Optional file logging with rotation capability +//! +//! # Example +//! +//! ```rust,ignore +//! use clap::Parser; +//! use base_cli_utils::LoggingArgs; +//! +//! #[derive(Parser)] +//! struct Cli { +//! #[command(flatten)] +//! logging: LoggingArgs, +//! } +//! +//! let cli = Cli::parse(); +//! let level = cli.logging.log_level(); +//! ``` + +use std::path::PathBuf; + +use clap::{ArgAction, Parser, ValueEnum}; + +/// Log output format. +/// +/// Determines how log messages are formatted when written to stdout/stderr or files. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] +pub enum LogFormat { + /// Full format with all metadata (timestamp, level, target, spans). + /// + /// Example: `2024-01-15T10:30:00.123456Z INFO node_reth::sync: Starting sync` + #[default] + Full, + + /// Compact format with minimal metadata. + /// + /// Example: `INFO sync: Starting sync` + Compact, + + /// JSON format for structured logging and log aggregation systems. + /// + /// Example: `{"timestamp":"2024-01-15T10:30:00.123456Z","level":"INFO","message":"Starting sync"}` + Json, +} + +/// Log rotation strategy for file logging. +/// +/// Specifies when log files should be rotated to prevent unbounded growth. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] +pub enum LogRotation { + /// Rotate logs every minute (primarily for testing). + Minutely, + + /// Rotate logs every hour. + #[default] + Hourly, + + /// Rotate logs every day at midnight. + Daily, + + /// Never rotate logs (single file that grows indefinitely). + Never, +} + +/// Logging configuration arguments for CLI applications. +/// +/// Provides a standardized way to configure logging across all node-reth CLI tools. +/// Can be embedded into other argument structs using clap's `#[command(flatten)]`. +/// +/// # Verbosity Levels +/// +/// The verbosity flag controls the log level: +/// - No flag: `WARN` level +/// - `-v`: `INFO` level +/// - `-vv`: `DEBUG` level +/// - `-vvv` or more: `TRACE` level +/// +/// # Examples +/// +/// Basic usage with clap: +/// +/// ```rust,ignore +/// use clap::Parser; +/// use base_cli_utils::LoggingArgs; +/// +/// #[derive(Parser)] +/// struct MyCli { +/// #[command(flatten)] +/// logging: LoggingArgs, +/// } +/// ``` +/// +/// Command line examples: +/// +/// ```text +/// # Default (WARN level, full format) +/// my-app +/// +/// # INFO level with compact format +/// my-app -v --log-format compact +/// +/// # DEBUG level with JSON format and file logging +/// my-app -vv --log-format json --log-file /var/log/my-app.log +/// +/// # TRACE level with daily log rotation +/// my-app -vvv --log-file /var/log/my-app.log --log-rotation daily +/// ``` +#[derive(Debug, Clone, Default, PartialEq, Eq, Parser)] +pub struct LoggingArgs { + /// Increase logging verbosity. + /// + /// Use multiple times for more verbose output: + /// - `-v`: INFO level + /// - `-vv`: DEBUG level + /// - `-vvv`: TRACE level + #[arg(short = 'v', long = "verbose", action = ArgAction::Count, global = true)] + pub verbosity: u8, + + /// Log output format. + /// + /// Controls how log messages are formatted: + /// - `full`: Complete format with timestamp, level, target, and spans + /// - `compact`: Minimal format with just level and message + /// - `json`: Structured JSON format for log aggregation + #[arg(long = "log-format", default_value = "full", global = true)] + pub format: LogFormat, + + /// Path to write log output to a file. + /// + /// When specified, logs will be written to this file in addition to stdout. + /// The parent directory must exist. + #[arg(long = "log-file", global = true)] + pub log_file: Option, + + /// Log file rotation strategy. + /// + /// Only applicable when `--log-file` is specified: + /// - `minutely`: Rotate every minute (for testing) + /// - `hourly`: Rotate every hour + /// - `daily`: Rotate every day at midnight + /// - `never`: Never rotate (single growing file) + #[arg(long = "log-rotation", global = true)] + pub log_rotation: Option, +} + +impl LoggingArgs { + /// Converts the verbosity count to a [`tracing::Level`]. + /// + /// The mapping is: + /// - `0`: [`tracing::Level::WARN`] + /// - `1`: [`tracing::Level::INFO`] + /// - `2`: [`tracing::Level::DEBUG`] + /// - `3+`: [`tracing::Level::TRACE`] + /// + /// # Examples + /// + /// ```rust + /// use base_cli_utils::LoggingArgs; + /// + /// let args = LoggingArgs::default(); + /// assert_eq!(args.log_level(), tracing::Level::WARN); + /// + /// let args = LoggingArgs { verbosity: 2, ..Default::default() }; + /// assert_eq!(args.log_level(), tracing::Level::DEBUG); + /// ``` + #[inline] + pub 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 the verbosity count to a [`tracing::level_filters::LevelFilter`]. + /// + /// This is useful when configuring tracing subscribers that require a filter. + /// + /// # Examples + /// + /// ```rust + /// use base_cli_utils::LoggingArgs; + /// use tracing::level_filters::LevelFilter; + /// + /// let args = LoggingArgs::default(); + /// assert_eq!(args.log_level_filter(), LevelFilter::WARN); + /// ``` + #[inline] + pub 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. + /// + /// # Examples + /// + /// ```rust + /// use base_cli_utils::LoggingArgs; + /// use std::path::PathBuf; + /// + /// let args = LoggingArgs::default(); + /// assert!(!args.has_file_logging()); + /// + /// let args = LoggingArgs { + /// log_file: Some(PathBuf::from("/var/log/app.log")), + /// ..Default::default() + /// }; + /// assert!(args.has_file_logging()); + /// ``` + #[inline] + pub const fn has_file_logging(&self) -> bool { + self.log_file.is_some() + } + + /// Returns the configured log rotation strategy. + /// + /// If no rotation is explicitly configured, returns [`LogRotation::Hourly`] as the default. + /// + /// # Examples + /// + /// ```rust + /// use base_cli_utils::{LoggingArgs, LogRotation}; + /// + /// let args = LoggingArgs::default(); + /// assert_eq!(args.rotation(), LogRotation::Hourly); + /// + /// let args = LoggingArgs { + /// log_rotation: Some(LogRotation::Daily), + /// ..Default::default() + /// }; + /// assert_eq!(args.rotation(), LogRotation::Daily); + /// ``` + #[inline] + pub fn rotation(&self) -> LogRotation { + self.log_rotation.unwrap_or_default() + } + + /// Returns `true` if the log format is JSON. + /// + /// This is useful for conditional configuration of tracing subscribers. + /// + /// # Examples + /// + /// ```rust + /// use base_cli_utils::{LoggingArgs, LogFormat}; + /// + /// let args = LoggingArgs::default(); + /// assert!(!args.is_json_format()); + /// + /// let args = LoggingArgs { + /// format: LogFormat::Json, + /// ..Default::default() + /// }; + /// assert!(args.is_json_format()); + /// ``` + #[inline] + pub const fn is_json_format(&self) -> bool { + matches!(self.format, LogFormat::Json) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_log_level() { + let args = LoggingArgs::default(); + assert_eq!(args.log_level(), tracing::Level::WARN); + assert_eq!(args.log_level_filter(), tracing::level_filters::LevelFilter::WARN); + } + + #[test] + fn test_verbosity_levels() { + let args = LoggingArgs { verbosity: 1, ..Default::default() }; + assert_eq!(args.log_level(), tracing::Level::INFO); + + let args = LoggingArgs { verbosity: 2, ..Default::default() }; + assert_eq!(args.log_level(), tracing::Level::DEBUG); + + let args = LoggingArgs { verbosity: 3, ..Default::default() }; + assert_eq!(args.log_level(), tracing::Level::TRACE); + + // Values above 3 should still be TRACE + let args = LoggingArgs { verbosity: 10, ..Default::default() }; + assert_eq!(args.log_level(), tracing::Level::TRACE); + } + + #[test] + fn test_default_format() { + let args = LoggingArgs::default(); + assert_eq!(args.format, LogFormat::Full); + assert!(!args.is_json_format()); + } + + #[test] + fn test_json_format() { + let args = LoggingArgs { format: LogFormat::Json, ..Default::default() }; + assert!(args.is_json_format()); + } + + #[test] + fn test_file_logging() { + let args = LoggingArgs::default(); + assert!(!args.has_file_logging()); + + let args = LoggingArgs { + log_file: Some(PathBuf::from("/tmp/test.log")), + ..Default::default() + }; + assert!(args.has_file_logging()); + } + + #[test] + fn test_log_rotation() { + let args = LoggingArgs::default(); + assert_eq!(args.rotation(), LogRotation::Hourly); + + let args = LoggingArgs { log_rotation: Some(LogRotation::Daily), ..Default::default() }; + assert_eq!(args.rotation(), LogRotation::Daily); + } +} diff --git a/crates/shared/cli-utils/src/runtime.rs b/crates/shared/cli-utils/src/runtime.rs new file mode 100644 index 00000000..aab522fe --- /dev/null +++ b/crates/shared/cli-utils/src/runtime.rs @@ -0,0 +1,141 @@ +//! Tokio runtime utilities for CLI applications. +//! +//! This module provides utilities for building and managing Tokio runtimes +//! with graceful shutdown handling via Ctrl+C signal interception. + +use std::future::Future; + +/// Builds a multi-threaded Tokio runtime with all features enabled. +/// +/// This function creates a Tokio runtime configured for optimal performance +/// in production workloads, with thread scheduling enabled across all +/// available CPU cores. +/// +/// # Returns +/// +/// Returns an [`eyre::Result`] containing the configured [`tokio::runtime::Runtime`], +/// or an error if the runtime could not be built. +/// +/// # Example +/// +/// ```no_run +/// use base_cli_utils::runtime::build_runtime; +/// +/// let runtime = build_runtime().expect("Failed to build runtime"); +/// runtime.block_on(async { +/// println!("Hello from the runtime!"); +/// }); +/// ``` +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 with graceful Ctrl+C shutdown handling. +/// +/// This function executes the provided future while simultaneously listening +/// for a Ctrl+C signal. If Ctrl+C is received, the function returns immediately +/// with `Ok(())`, allowing the application to perform graceful shutdown. +/// +/// # Arguments +/// +/// * `fut` - The future to run to completion. +/// +/// # Returns +/// +/// Returns `Ok(())` on successful completion or Ctrl+C interruption. +/// Returns an error if the Ctrl+C handler fails to install. +/// +/// # Example +/// +/// ```no_run +/// use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c}; +/// +/// async fn my_long_running_task() { +/// loop { +/// // Do some work +/// tokio::time::sleep(std::time::Duration::from_secs(1)).await; +/// } +/// } +/// +/// let runtime = build_runtime().expect("Failed to build runtime"); +/// runtime.block_on(async { +/// run_until_ctrl_c(my_long_running_task()).await.expect("Runtime error"); +/// }); +/// ``` +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 with graceful Ctrl+C shutdown handling. +/// +/// This is similar to [`run_until_ctrl_c`], but accepts a future that returns +/// a [`Result`]. If the future completes with an error, that error is propagated. +/// If Ctrl+C is received, the function returns `Ok(())`. +/// +/// # Arguments +/// +/// * `fut` - The fallible future to run to completion. +/// +/// # Returns +/// +/// Returns `Ok(())` on successful completion or Ctrl+C interruption. +/// Returns the future's error if it fails, or an error if the Ctrl+C handler +/// fails to install. +/// +/// # Example +/// +/// ```no_run +/// use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c_fallible}; +/// +/// async fn my_fallible_task() -> eyre::Result<()> { +/// // Do some work that might fail +/// Ok(()) +/// } +/// +/// let runtime = build_runtime().expect("Failed to build runtime"); +/// runtime.block_on(async { +/// run_until_ctrl_c_fallible(my_fallible_task()).await.expect("Runtime error"); +/// }); +/// ``` +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 + } + } +} From 32569df2824c734907e27375a0f8d4b3017a7ed9 Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Fri, 9 Jan 2026 18:11:55 -0500 Subject: [PATCH 2/3] feat(cli): add some basic utility methods --- crates/shared/cli-utils/src/logging.rs | 10 ++++------ crates/shared/cli-utils/src/runtime.rs | 8 ++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/crates/shared/cli-utils/src/logging.rs b/crates/shared/cli-utils/src/logging.rs index 7f404c3e..3444b576 100644 --- a/crates/shared/cli-utils/src/logging.rs +++ b/crates/shared/cli-utils/src/logging.rs @@ -168,7 +168,7 @@ impl LoggingArgs { /// assert_eq!(args.log_level(), tracing::Level::DEBUG); /// ``` #[inline] - pub fn log_level(&self) -> tracing::Level { + pub const fn log_level(&self) -> tracing::Level { match self.verbosity { 0 => tracing::Level::WARN, 1 => tracing::Level::INFO, @@ -191,7 +191,7 @@ impl LoggingArgs { /// assert_eq!(args.log_level_filter(), LevelFilter::WARN); /// ``` #[inline] - pub fn log_level_filter(&self) -> tracing::level_filters::LevelFilter { + 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, @@ -314,10 +314,8 @@ mod tests { let args = LoggingArgs::default(); assert!(!args.has_file_logging()); - let args = LoggingArgs { - log_file: Some(PathBuf::from("/tmp/test.log")), - ..Default::default() - }; + 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 index aab522fe..6dde5ba4 100644 --- a/crates/shared/cli-utils/src/runtime.rs +++ b/crates/shared/cli-utils/src/runtime.rs @@ -70,9 +70,7 @@ where F: Future, { let ctrl_c = async { - tokio::signal::ctrl_c() - .await - .expect("Failed to install Ctrl+C handler"); + tokio::signal::ctrl_c().await.expect("Failed to install Ctrl+C handler"); }; tokio::select! { @@ -123,9 +121,7 @@ where F: Future>, { let ctrl_c = async { - tokio::signal::ctrl_c() - .await - .expect("Failed to install Ctrl+C handler"); + tokio::signal::ctrl_c().await.expect("Failed to install Ctrl+C handler"); }; tokio::select! { From e5b81a705b56b3bb2a8eafa7b523401cb987ba7e Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Fri, 9 Jan 2026 18:18:25 -0500 Subject: [PATCH 3/3] feat(cli): add some basic utility methods --- crates/shared/cli-utils/README.md | 126 ++---------- crates/shared/cli-utils/src/args.rs | 48 +---- crates/shared/cli-utils/src/logging.rs | 260 ++++--------------------- crates/shared/cli-utils/src/runtime.rs | 108 +--------- 4 files changed, 54 insertions(+), 488 deletions(-) diff --git a/crates/shared/cli-utils/README.md b/crates/shared/cli-utils/README.md index 81d827b0..3252be4e 100644 --- a/crates/shared/cli-utils/README.md +++ b/crates/shared/cli-utils/README.md @@ -3,44 +3,25 @@ CI MIT License -CLI utilities for the Base Reth node. Provides shared infrastructure for versioning, argument parsing, logging configuration, and runtime management. +CLI utilities for the Base Reth node. ## Overview -- **`Version`**: Handles versioning metadata for the Base Reth node, including client version strings and P2P identification. -- **`GlobalArgs`**: Common CLI arguments with network chain ID and logging configuration. -- **`LoggingArgs`**: Structured logging configuration with verbosity levels, output formats, and file rotation. -- **`runtime`**: Tokio runtime utilities with graceful Ctrl+C shutdown handling. +- **`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-utils = { git = "https://github.com/base/node-reth" } ``` -### Version Initialization - -Initialize versioning at node startup: - ```rust,ignore -use base_cli_utils::Version; - -// Initialize version metadata before starting the node -Version::init(); - -// Access the client version string -let client_version = Version::NODE_RETH_CLIENT_VERSION; -``` - -### Global Arguments - -Use `GlobalArgs` for common CLI configuration: - -```rust,ignore -use base_cli_utils::GlobalArgs; +use base_cli_utils::{GlobalArgs, Version}; +use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c}; use clap::Parser; #[derive(Parser)] @@ -49,100 +30,15 @@ struct MyCli { global: GlobalArgs, } -let cli = MyCli::parse(); -println!("Chain ID: {}", cli.global.chain_id); -println!("Log level: {:?}", cli.global.logging.log_level()); -``` - -Command line usage: - -```bash -# Default (Base Mainnet, WARN level) -my-app - -# Base Sepolia with INFO logging -my-app --chain-id 84532 -v - -# Using environment variable -BASE_NETWORK=84532 my-app -vv -``` - -### Logging Configuration - -Use `LoggingArgs` for standalone logging configuration: - -```rust,ignore -use base_cli_utils::LoggingArgs; -use clap::Parser; - -#[derive(Parser)] -struct MyCli { - #[command(flatten)] - logging: LoggingArgs, -} - -let cli = MyCli::parse(); -let level = cli.logging.log_level(); -let filter = cli.logging.log_level_filter(); -``` - -Verbosity levels: -- No flag: `WARN` level -- `-v`: `INFO` level -- `-vv`: `DEBUG` level -- `-vvv`: `TRACE` level - -Output formats: -- `--log-format full`: Complete format with timestamp, level, target, and spans -- `--log-format compact`: Minimal format with just level and message -- `--log-format json`: Structured JSON format for log aggregation - -File logging with rotation: - -```bash -my-app -vv --log-file /var/log/app.log --log-rotation daily -``` - -### Runtime Utilities - -Build and run a Tokio runtime with graceful shutdown: - -```rust,ignore -use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c}; - -async fn my_long_running_task() { - loop { - // Do some work - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } -} - fn main() -> eyre::Result<()> { - let runtime = build_runtime()?; - runtime.block_on(async { - run_until_ctrl_c(my_long_running_task()).await - }) -} -``` + Version::init(); + let cli = MyCli::parse(); -For fallible tasks: - -```rust,ignore -use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c_fallible}; - -async fn my_fallible_task() -> eyre::Result<()> { - // Do some work that might fail - Ok(()) -} - -fn main() -> eyre::Result<()> { let runtime = build_runtime()?; - runtime.block_on(async { - run_until_ctrl_c_fallible(my_fallible_task()).await - }) + 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 index 8a82ea27..de4895f1 100644 --- a/crates/shared/cli-utils/src/args.rs +++ b/crates/shared/cli-utils/src/args.rs @@ -1,59 +1,17 @@ //! Global CLI arguments for the Base Reth node. -//! -//! This module provides common command-line arguments that are shared across -//! different CLI commands and subcommands. -//! -//! # Example -//! -//! ```ignore -//! use base_cli_utils::GlobalArgs; -//! use clap::Parser; -//! -//! #[derive(Debug, Parser)] -//! struct MyCli { -//! #[command(flatten)] -//! global: GlobalArgs, -//! } -//! ``` use super::LoggingArgs; /// Global arguments shared across all CLI commands. /// -/// These arguments provide common configuration options that apply to the entire -/// application, such as network selection and logging configuration. -/// -/// # Network Configuration -/// -/// The chain ID can be configured via the `--chain-id` flag or the `BASE_NETWORK` -/// environment variable. If neither is specified, it defaults to Base Mainnet (8453). -/// -/// # Example -/// -/// ```ignore -/// // Using default Base Mainnet -/// $ my-cli -/// -/// // Using Base Sepolia testnet -/// $ my-cli --chain-id 84532 -/// -/// // Using environment variable -/// $ BASE_NETWORK=84532 my-cli -/// ``` +/// 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 { - /// The chain ID of the network to connect to. - /// - /// Defaults to Base Mainnet (8453). Can also be set via the `BASE_NETWORK` - /// environment variable. - /// - /// Common values: - /// - 8453: Base Mainnet - /// - 84532: Base Sepolia + /// Chain ID (8453 = Base Mainnet, 84532 = Base Sepolia). #[arg(long = "chain-id", env = "BASE_NETWORK", default_value = "8453")] pub chain_id: u64, - /// Logging configuration arguments. + /// Logging configuration. #[command(flatten)] pub logging: LoggingArgs, } diff --git a/crates/shared/cli-utils/src/logging.rs b/crates/shared/cli-utils/src/logging.rs index 3444b576..0ab096e1 100644 --- a/crates/shared/cli-utils/src/logging.rs +++ b/crates/shared/cli-utils/src/logging.rs @@ -1,172 +1,59 @@ -//! Logging configuration utilities for CLI applications. -//! -//! This module provides structured logging configuration with support for: -//! - Verbosity levels (`-v`, `-vv`, `-vvv`) -//! - Multiple output formats (full, compact, json) -//! - Optional file logging with rotation capability -//! -//! # Example -//! -//! ```rust,ignore -//! use clap::Parser; -//! use base_cli_utils::LoggingArgs; -//! -//! #[derive(Parser)] -//! struct Cli { -//! #[command(flatten)] -//! logging: LoggingArgs, -//! } -//! -//! let cli = Cli::parse(); -//! let level = cli.logging.log_level(); -//! ``` +//! Logging configuration for CLI applications. use std::path::PathBuf; use clap::{ArgAction, Parser, ValueEnum}; /// Log output format. -/// -/// Determines how log messages are formatted when written to stdout/stderr or files. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] pub enum LogFormat { - /// Full format with all metadata (timestamp, level, target, spans). - /// - /// Example: `2024-01-15T10:30:00.123456Z INFO node_reth::sync: Starting sync` + /// Full format with timestamp, level, target, and spans. #[default] Full, - /// Compact format with minimal metadata. - /// - /// Example: `INFO sync: Starting sync` Compact, - - /// JSON format for structured logging and log aggregation systems. - /// - /// Example: `{"timestamp":"2024-01-15T10:30:00.123456Z","level":"INFO","message":"Starting sync"}` + /// JSON format for structured logging. Json, } -/// Log rotation strategy for file logging. -/// -/// Specifies when log files should be rotated to prevent unbounded growth. +/// Log rotation strategy. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] pub enum LogRotation { - /// Rotate logs every minute (primarily for testing). + /// Rotate every minute (for testing). Minutely, - - /// Rotate logs every hour. + /// Rotate every hour. #[default] Hourly, - - /// Rotate logs every day at midnight. + /// Rotate every day. Daily, - - /// Never rotate logs (single file that grows indefinitely). + /// Never rotate. Never, } -/// Logging configuration arguments for CLI applications. -/// -/// Provides a standardized way to configure logging across all node-reth CLI tools. -/// Can be embedded into other argument structs using clap's `#[command(flatten)]`. -/// -/// # Verbosity Levels -/// -/// The verbosity flag controls the log level: -/// - No flag: `WARN` level -/// - `-v`: `INFO` level -/// - `-vv`: `DEBUG` level -/// - `-vvv` or more: `TRACE` level -/// -/// # Examples -/// -/// Basic usage with clap: -/// -/// ```rust,ignore -/// use clap::Parser; -/// use base_cli_utils::LoggingArgs; -/// -/// #[derive(Parser)] -/// struct MyCli { -/// #[command(flatten)] -/// logging: LoggingArgs, -/// } -/// ``` -/// -/// Command line examples: -/// -/// ```text -/// # Default (WARN level, full format) -/// my-app -/// -/// # INFO level with compact format -/// my-app -v --log-format compact -/// -/// # DEBUG level with JSON format and file logging -/// my-app -vv --log-format json --log-file /var/log/my-app.log +/// Logging configuration arguments. /// -/// # TRACE level with daily log rotation -/// my-app -vvv --log-file /var/log/my-app.log --log-rotation daily -/// ``` +/// Verbosity: `-v` (INFO), `-vv` (DEBUG), `-vvv` (TRACE). Default is WARN. #[derive(Debug, Clone, Default, PartialEq, Eq, Parser)] pub struct LoggingArgs { - /// Increase logging verbosity. - /// - /// Use multiple times for more verbose output: - /// - `-v`: INFO level - /// - `-vv`: DEBUG level - /// - `-vvv`: TRACE level + /// Increase logging verbosity (-v, -vv, -vvv). #[arg(short = 'v', long = "verbose", action = ArgAction::Count, global = true)] pub verbosity: u8, - /// Log output format. - /// - /// Controls how log messages are formatted: - /// - `full`: Complete format with timestamp, level, target, and spans - /// - `compact`: Minimal format with just level and message - /// - `json`: Structured JSON format for log aggregation + /// Log output format (full, compact, json). #[arg(long = "log-format", default_value = "full", global = true)] pub format: LogFormat, - /// Path to write log output to a file. - /// - /// When specified, logs will be written to this file in addition to stdout. - /// The parent directory must exist. + /// Path to write logs to a file. #[arg(long = "log-file", global = true)] pub log_file: Option, - /// Log file rotation strategy. - /// - /// Only applicable when `--log-file` is specified: - /// - `minutely`: Rotate every minute (for testing) - /// - `hourly`: Rotate every hour - /// - `daily`: Rotate every day at midnight - /// - `never`: Never rotate (single growing file) + /// Log file rotation (minutely, hourly, daily, never). #[arg(long = "log-rotation", global = true)] pub log_rotation: Option, } impl LoggingArgs { - /// Converts the verbosity count to a [`tracing::Level`]. - /// - /// The mapping is: - /// - `0`: [`tracing::Level::WARN`] - /// - `1`: [`tracing::Level::INFO`] - /// - `2`: [`tracing::Level::DEBUG`] - /// - `3+`: [`tracing::Level::TRACE`] - /// - /// # Examples - /// - /// ```rust - /// use base_cli_utils::LoggingArgs; - /// - /// let args = LoggingArgs::default(); - /// assert_eq!(args.log_level(), tracing::Level::WARN); - /// - /// let args = LoggingArgs { verbosity: 2, ..Default::default() }; - /// assert_eq!(args.log_level(), tracing::Level::DEBUG); - /// ``` + /// Converts verbosity to a [`tracing::Level`]. #[inline] pub const fn log_level(&self) -> tracing::Level { match self.verbosity { @@ -177,19 +64,7 @@ impl LoggingArgs { } } - /// Converts the verbosity count to a [`tracing::level_filters::LevelFilter`]. - /// - /// This is useful when configuring tracing subscribers that require a filter. - /// - /// # Examples - /// - /// ```rust - /// use base_cli_utils::LoggingArgs; - /// use tracing::level_filters::LevelFilter; - /// - /// let args = LoggingArgs::default(); - /// assert_eq!(args.log_level_filter(), LevelFilter::WARN); - /// ``` + /// Converts verbosity to a [`tracing::level_filters::LevelFilter`]. #[inline] pub const fn log_level_filter(&self) -> tracing::level_filters::LevelFilter { match self.verbosity { @@ -201,68 +76,18 @@ impl LoggingArgs { } /// Returns `true` if file logging is enabled. - /// - /// # Examples - /// - /// ```rust - /// use base_cli_utils::LoggingArgs; - /// use std::path::PathBuf; - /// - /// let args = LoggingArgs::default(); - /// assert!(!args.has_file_logging()); - /// - /// let args = LoggingArgs { - /// log_file: Some(PathBuf::from("/var/log/app.log")), - /// ..Default::default() - /// }; - /// assert!(args.has_file_logging()); - /// ``` #[inline] pub const fn has_file_logging(&self) -> bool { self.log_file.is_some() } - /// Returns the configured log rotation strategy. - /// - /// If no rotation is explicitly configured, returns [`LogRotation::Hourly`] as the default. - /// - /// # Examples - /// - /// ```rust - /// use base_cli_utils::{LoggingArgs, LogRotation}; - /// - /// let args = LoggingArgs::default(); - /// assert_eq!(args.rotation(), LogRotation::Hourly); - /// - /// let args = LoggingArgs { - /// log_rotation: Some(LogRotation::Daily), - /// ..Default::default() - /// }; - /// assert_eq!(args.rotation(), LogRotation::Daily); - /// ``` + /// 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. - /// - /// This is useful for conditional configuration of tracing subscribers. - /// - /// # Examples - /// - /// ```rust - /// use base_cli_utils::{LoggingArgs, LogFormat}; - /// - /// let args = LoggingArgs::default(); - /// assert!(!args.is_json_format()); - /// - /// let args = LoggingArgs { - /// format: LogFormat::Json, - /// ..Default::default() - /// }; - /// assert!(args.is_json_format()); - /// ``` #[inline] pub const fn is_json_format(&self) -> bool { matches!(self.format, LogFormat::Json) @@ -273,58 +98,39 @@ impl LoggingArgs { mod tests { use super::*; - #[test] - fn test_default_log_level() { - let args = LoggingArgs::default(); - assert_eq!(args.log_level(), tracing::Level::WARN); - assert_eq!(args.log_level_filter(), tracing::level_filters::LevelFilter::WARN); - } - #[test] fn test_verbosity_levels() { - let args = LoggingArgs { verbosity: 1, ..Default::default() }; - assert_eq!(args.log_level(), tracing::Level::INFO); - - let args = LoggingArgs { verbosity: 2, ..Default::default() }; - assert_eq!(args.log_level(), tracing::Level::DEBUG); - - let args = LoggingArgs { verbosity: 3, ..Default::default() }; - assert_eq!(args.log_level(), tracing::Level::TRACE); - - // Values above 3 should still be TRACE - let args = LoggingArgs { verbosity: 10, ..Default::default() }; - assert_eq!(args.log_level(), tracing::Level::TRACE); + 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_default_format() { + 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); - #[test] - fn test_json_format() { let args = LoggingArgs { format: LogFormat::Json, ..Default::default() }; assert!(args.is_json_format()); } #[test] fn test_file_logging() { - let args = LoggingArgs::default(); - assert!(!args.has_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()); } - - #[test] - fn test_log_rotation() { - let args = LoggingArgs::default(); - assert_eq!(args.rotation(), LogRotation::Hourly); - - let args = LoggingArgs { log_rotation: Some(LogRotation::Daily), ..Default::default() }; - assert_eq!(args.rotation(), LogRotation::Daily); - } } diff --git a/crates/shared/cli-utils/src/runtime.rs b/crates/shared/cli-utils/src/runtime.rs index 6dde5ba4..ac2b61e4 100644 --- a/crates/shared/cli-utils/src/runtime.rs +++ b/crates/shared/cli-utils/src/runtime.rs @@ -1,31 +1,8 @@ -//! Tokio runtime utilities for CLI applications. -//! -//! This module provides utilities for building and managing Tokio runtimes -//! with graceful shutdown handling via Ctrl+C signal interception. +//! Tokio runtime utilities with graceful Ctrl+C shutdown handling. use std::future::Future; /// Builds a multi-threaded Tokio runtime with all features enabled. -/// -/// This function creates a Tokio runtime configured for optimal performance -/// in production workloads, with thread scheduling enabled across all -/// available CPU cores. -/// -/// # Returns -/// -/// Returns an [`eyre::Result`] containing the configured [`tokio::runtime::Runtime`], -/// or an error if the runtime could not be built. -/// -/// # Example -/// -/// ```no_run -/// use base_cli_utils::runtime::build_runtime; -/// -/// let runtime = build_runtime().expect("Failed to build runtime"); -/// runtime.block_on(async { -/// println!("Hello from the runtime!"); -/// }); -/// ``` pub fn build_runtime() -> eyre::Result { tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -33,38 +10,7 @@ pub fn build_runtime() -> eyre::Result { .map_err(|e| eyre::eyre!("Failed to build tokio runtime: {}", e)) } -/// Runs a future to completion with graceful Ctrl+C shutdown handling. -/// -/// This function executes the provided future while simultaneously listening -/// for a Ctrl+C signal. If Ctrl+C is received, the function returns immediately -/// with `Ok(())`, allowing the application to perform graceful shutdown. -/// -/// # Arguments -/// -/// * `fut` - The future to run to completion. -/// -/// # Returns -/// -/// Returns `Ok(())` on successful completion or Ctrl+C interruption. -/// Returns an error if the Ctrl+C handler fails to install. -/// -/// # Example -/// -/// ```no_run -/// use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c}; -/// -/// async fn my_long_running_task() { -/// loop { -/// // Do some work -/// tokio::time::sleep(std::time::Duration::from_secs(1)).await; -/// } -/// } -/// -/// let runtime = build_runtime().expect("Failed to build runtime"); -/// runtime.block_on(async { -/// run_until_ctrl_c(my_long_running_task()).await.expect("Runtime error"); -/// }); -/// ``` +/// Runs a future to completion, returning early on Ctrl+C. pub async fn run_until_ctrl_c(fut: F) -> eyre::Result<()> where F: Future, @@ -75,47 +21,12 @@ where tokio::select! { biased; - - () = ctrl_c => { - Ok(()) - } - () = fut => { - Ok(()) - } + () = ctrl_c => Ok(()), + () = fut => Ok(()), } } -/// Runs a fallible future to completion with graceful Ctrl+C shutdown handling. -/// -/// This is similar to [`run_until_ctrl_c`], but accepts a future that returns -/// a [`Result`]. If the future completes with an error, that error is propagated. -/// If Ctrl+C is received, the function returns `Ok(())`. -/// -/// # Arguments -/// -/// * `fut` - The fallible future to run to completion. -/// -/// # Returns -/// -/// Returns `Ok(())` on successful completion or Ctrl+C interruption. -/// Returns the future's error if it fails, or an error if the Ctrl+C handler -/// fails to install. -/// -/// # Example -/// -/// ```no_run -/// use base_cli_utils::runtime::{build_runtime, run_until_ctrl_c_fallible}; -/// -/// async fn my_fallible_task() -> eyre::Result<()> { -/// // Do some work that might fail -/// Ok(()) -/// } -/// -/// let runtime = build_runtime().expect("Failed to build runtime"); -/// runtime.block_on(async { -/// run_until_ctrl_c_fallible(my_fallible_task()).await.expect("Runtime error"); -/// }); -/// ``` +/// 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>, @@ -126,12 +37,7 @@ where tokio::select! { biased; - - () = ctrl_c => { - Ok(()) - } - result = fut => { - result - } + () = ctrl_c => Ok(()), + result = fut => result, } }