Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/shared/cli-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
41 changes: 25 additions & 16 deletions crates/shared/cli-utils/README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
# `base-cli`
# `base-cli-utils`

<a href="https://github.com/base/node-reth/actions/workflows/ci.yml"><img src="https://github.com/base/node-reth/actions/workflows/ci.yml/badge.svg?label=ci" alt="CI"></a>
<a href="https://github.com/base/node-reth/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-d1d1f6.svg?label=license&labelColor=2a2f35" alt="MIT License"></a>

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)
17 changes: 17 additions & 0 deletions crates/shared/cli-utils/src/args.rs
Original file line number Diff line number Diff line change
@@ -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,
}
8 changes: 8 additions & 0 deletions crates/shared/cli-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
136 changes: 136 additions & 0 deletions crates/shared/cli-utils/src/logging.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,

/// Log file rotation (minutely, hourly, daily, never).
#[arg(long = "log-rotation", global = true)]
pub log_rotation: Option<LogRotation>,
}

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());
}
}
43 changes: 43 additions & 0 deletions crates/shared/cli-utils/src/runtime.rs
Original file line number Diff line number Diff line change
@@ -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::Runtime> {
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<F>(fut: F) -> eyre::Result<()>
where
F: Future<Output = ()>,
{
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<F>(fut: F) -> eyre::Result<()>
where
F: Future<Output = eyre::Result<()>>,
{
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,
}
}