From c236939469fd193c36c7db5e74ef3bd44ce63f4b Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Fri, 22 May 2026 10:53:28 +0000 Subject: [PATCH 1/3] cli: add --solana-url and --log-verbose global flags --- CHANGELOG.md | 3 +++ Cargo.lock | 2 ++ client/doublezero/Cargo.toml | 2 ++ client/doublezero/src/main.rs | 14 +++++++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa2d168d6..b756a0b005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to this project will be documented in this file. - SDK (Rust) - Drop the pre-submit `simulate_transaction` call in `DZClient::execute_transaction_inner` and submit with `skip_preflight: true`, eliminating the redundant double-simulation (the explicit simulate plus `send_and_confirm_transaction`'s default preflight) on the happy path. Program logs are now recovered from `get_transaction` on the failure path so `SimulationError` / `SimulationTransactionError` and `DoubleZeroError` mapping in CLI output are unchanged. Trade-off: failing transactions now land onchain and burn fees instead of failing for free at simulation ([#3750](https://github.com/malbeclabs/doublezero/pull/3750)) - e2e/qa: remove client-side capacity pre-filtering from `ValidDevices`, because the QA user pubkey bypasses capacity limits using the serviceability global-config qa-allowlist. Individual device failures no longer fail the test; instead, overall and per-host failure rates are evaluated after all batches and the test only fails if either exceeds `--failure-threshold` (default 10%) or `--per-host-failure-threshold` (default 20%). +- CLI + - Add `--solana-url ` global flag to `doublezero` per RFC-20 §Global flags. Distinct from `--url`, which continues to override the DZ ledger transport; `--solana-url` targets the Solana L1 transport. The flag is parsed and exposed on the binary's `App` struct; per-verb consumption lands when verbs migrate to construct typed Solana L1 clients from `CliContext`. + - Add `--log-verbose` (repeatable) global flag and initialize the `tracing` subscriber at startup. Default level is `warn`; one `--log-verbose` raises to `debug`, two raise to `trace`. Diagnostic logs go to stderr so `--json` output on stdout remains parseable. Honors the `RUST_LOG` environment variable when set, overriding the CLI-flag verbosity for per-module filtering. Replaces the previous `println!("using keypair: ...")` stdout line with a `tracing::info!` event; the keypair confirmation now appears only at `--log-verbose` or higher and no longer pollutes parseable stdout. (Named `--log-verbose` rather than the RFC-20 §Global-flags suggested `--verbose` / `-v` because the existing `doublezero connect` / `disconnect` subcommands already own a `--verbose` flag with `bool` type; the global flag deviation will be revisited when the daemon-control module crate is carved out.) ## [v0.24.0](https://github.com/malbeclabs/doublezero/compare/client/v0.23.0...client/v0.24.0) - 2026-05-22 diff --git a/Cargo.lock b/Cargo.lock index 57a003c01f..4f86e2d2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1480,6 +1480,7 @@ dependencies = [ "clap_complete", "console", "ctor", + "doublezero-cli-core", "doublezero-config", "doublezero-program-common", "doublezero-serviceability", @@ -1506,6 +1507,7 @@ dependencies = [ "tabled", "tempfile", "tokio", + "tracing", ] [[package]] diff --git a/client/doublezero/Cargo.toml b/client/doublezero/Cargo.toml index aa41d6cafb..8e669cf5e3 100644 --- a/client/doublezero/Cargo.toml +++ b/client/doublezero/Cargo.toml @@ -42,9 +42,11 @@ tokio.workspace = true # Dependencies from this workspace doublezero_sdk.workspace = true doublezero_cli.workspace = true +doublezero-cli-core.workspace = true doublezero-config.workspace = true doublezero-serviceability.workspace = true doublezero-program-common.workspace = true +tracing.workspace = true [features] default-mainnet-beta = ["doublezero_sdk/default-mainnet-beta", "doublezero_cli/default-mainnet-beta"] diff --git a/client/doublezero/src/main.rs b/client/doublezero/src/main.rs index 9e7a9ba521..1d6772c47b 100644 --- a/client/doublezero/src/main.rs +++ b/client/doublezero/src/main.rs @@ -49,6 +49,9 @@ struct App { /// DZ ledger WebSocket URL #[arg(long, value_name = "WEBSOCKET_URL", global = true)] ws: Option, + /// Solana L1 RPC URL override (does not affect the DZ ledger) + #[arg(long, value_name = "SOLANA_RPC_URL", global = true)] + solana_url: Option, /// DZ program ID (testnet or devnet) #[arg(long, value_name = "PROGRAM_ID", global = true)] program_id: Option, @@ -70,6 +73,13 @@ struct App { /// Suppress version warning output #[arg(long, global = true)] no_version_warning: bool, + /// Increase diagnostic logging verbosity. Repeat for higher levels: + /// `--log-verbose` raises to debug, `--log-verbose --log-verbose` to trace. + /// Renamed from `--verbose` to avoid colliding with the per-subcommand + /// `--verbose` flags inherited from earlier releases of `doublezero + /// connect` / `disconnect`. + #[arg(long = "log-verbose", action = clap::ArgAction::Count, global = true)] + log_verbosity: u8, /// Print version information #[arg(short = 'V', long = "version", action = clap::ArgAction::SetTrue)] version: bool, @@ -83,12 +93,14 @@ async fn main() -> eyre::Result<()> { let app = App::parse(); + doublezero_cli_core::init_logging(app.log_verbosity); + if let Some(sock_file) = &app.sock_file { ServiceControllerImpl::set_global_socket_path(sock_file.to_string_lossy()); } if let Some(keypair) = &app.keypair { - println!("using keypair: {}", keypair.display()); + tracing::info!(keypair = %keypair.display(), "using keypair"); } let (url, ws, program_id) = if let Some(env) = app.env { From 2c8bb4023dc13e9700a8d8ca8f1bb75c83c40154 Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Sun, 24 May 2026 14:34:02 +0000 Subject: [PATCH 2/3] cli: make --env mutually exclusive with per-field URL/program overrides `--env` now conflicts with `--url`, `--ws`, `--solana-url`, `--program-id`, and `--geo-program-id` via clap. Combining them used to be silently inconsistent: per-field overrides won over env defaults on some fields and not others, so e.g. `--env devnet --url ` would pair the custom RPC URL with devnet's default WebSocket URL. Clap now rejects the combination upfront with a clear usage error, matching the design rule that `--env` selects a full preset wholesale. Adds clap-parse tests covering each conflict pair and the positive single-flag cases. --- client/doublezero/src/main.rs | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/client/doublezero/src/main.rs b/client/doublezero/src/main.rs index 1d6772c47b..d6b8b7dae4 100644 --- a/client/doublezero/src/main.rs +++ b/client/doublezero/src/main.rs @@ -40,8 +40,19 @@ use servicecontroller::ServiceControllerImpl; struct App { #[command(subcommand)] command: Option, - /// DZ env (testnet, devnet, or mainnet-beta) - #[arg(short, long, value_name = "ENV", global = true)] + /// DZ env (testnet, devnet, or mainnet-beta). + /// + /// Mutually exclusive with the per-field URL and program-ID overrides + /// (`--url`, `--ws`, `--solana-url`, `--program-id`, `--geo-program-id`). + /// Pass `--env` to use a network's defaults wholesale, or pass the + /// individual overrides; combining the two yields an error from clap. + #[arg( + short, + long, + value_name = "ENV", + global = true, + conflicts_with_all = ["url", "ws", "solana_url", "program_id", "geo_program_id"], + )] env: Option, /// DZ ledger RPC URL #[arg(long, value_name = "RPC_URL", global = true)] @@ -440,3 +451,66 @@ async fn main() -> eyre::Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::App; + use clap::{error::ErrorKind, Parser}; + + fn parse_err(args: &[&str]) -> clap::Error { + App::try_parse_from(args).expect_err("expected clap to reject these arguments") + } + + #[test] + fn env_conflicts_with_url() { + let err = parse_err(&[ + "doublezero", + "--env", + "devnet", + "--url", + "https://x.invalid/", + ]); + assert_eq!(err.kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn env_conflicts_with_ws() { + let err = parse_err(&["doublezero", "--env", "devnet", "--ws", "wss://x.invalid/"]); + assert_eq!(err.kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn env_conflicts_with_solana_url() { + let err = parse_err(&[ + "doublezero", + "--env", + "devnet", + "--solana-url", + "https://x.invalid/", + ]); + assert_eq!(err.kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn env_conflicts_with_program_id() { + let err = parse_err(&[ + "doublezero", + "--env", + "devnet", + "--program-id", + "11111111111111111111111111111111", + ]); + assert_eq!(err.kind(), ErrorKind::ArgumentConflict); + } + + #[test] + fn env_alone_parses() { + App::try_parse_from(["doublezero", "--env", "devnet"]).expect("--env alone should parse"); + } + + #[test] + fn url_alone_parses() { + App::try_parse_from(["doublezero", "--url", "https://x.invalid/"]) + .expect("--url alone should parse"); + } +} From f1f6f3d4bb96b05f0baaa93958348572158234eb Mon Sep 17 00:00:00 2001 From: Juan Olveira Date: Tue, 26 May 2026 21:03:26 +0000 Subject: [PATCH 3/3] cli: replace --log-verbose count with --log-level Swap the repeatable --log-verbose flag for --log-level taking one of off/error/warn/info/debug/trace. init_logging now takes a LogLevel enum (clap ValueEnum) defined in doublezero-cli-core. Default level remains warn; RUST_LOG still overrides. --- CHANGELOG.md | 2 +- client/doublezero/src/main.rs | 19 +++++---- crates/doublezero-cli-core/src/lib.rs | 2 +- crates/doublezero-cli-core/src/logging.rs | 48 +++++++++++++++++------ 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b756a0b005..0d96a250df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ All notable changes to this project will be documented in this file. - e2e/qa: remove client-side capacity pre-filtering from `ValidDevices`, because the QA user pubkey bypasses capacity limits using the serviceability global-config qa-allowlist. Individual device failures no longer fail the test; instead, overall and per-host failure rates are evaluated after all batches and the test only fails if either exceeds `--failure-threshold` (default 10%) or `--per-host-failure-threshold` (default 20%). - CLI - Add `--solana-url ` global flag to `doublezero` per RFC-20 §Global flags. Distinct from `--url`, which continues to override the DZ ledger transport; `--solana-url` targets the Solana L1 transport. The flag is parsed and exposed on the binary's `App` struct; per-verb consumption lands when verbs migrate to construct typed Solana L1 clients from `CliContext`. - - Add `--log-verbose` (repeatable) global flag and initialize the `tracing` subscriber at startup. Default level is `warn`; one `--log-verbose` raises to `debug`, two raise to `trace`. Diagnostic logs go to stderr so `--json` output on stdout remains parseable. Honors the `RUST_LOG` environment variable when set, overriding the CLI-flag verbosity for per-module filtering. Replaces the previous `println!("using keypair: ...")` stdout line with a `tracing::info!` event; the keypair confirmation now appears only at `--log-verbose` or higher and no longer pollutes parseable stdout. (Named `--log-verbose` rather than the RFC-20 §Global-flags suggested `--verbose` / `-v` because the existing `doublezero connect` / `disconnect` subcommands already own a `--verbose` flag with `bool` type; the global flag deviation will be revisited when the daemon-control module crate is carved out.) + - Add `--log-level ` global flag and initialize the `tracing` subscriber at startup. `LEVEL` is one of `off`, `error`, `warn` (default), `info`, `debug`, `trace`. Diagnostic logs go to stderr so `--json` output on stdout remains parseable. Honors the `RUST_LOG` environment variable when set, overriding the CLI-flag level for per-module filtering. Replaces the previous `println!("using keypair: ...")` stdout line with a `tracing::info!` event; the keypair confirmation now appears only at `--log-level info` or higher and no longer pollutes parseable stdout. (Named `--log-level` rather than the RFC-20 §Global-flags suggested `--verbose` / `-v` because the existing `doublezero connect` / `disconnect` subcommands already own a `--verbose` flag with `bool` type; the global flag deviation will be revisited when the daemon-control module crate is carved out.) ## [v0.24.0](https://github.com/malbeclabs/doublezero/compare/client/v0.23.0...client/v0.24.0) - 2026-05-22 diff --git a/client/doublezero/src/main.rs b/client/doublezero/src/main.rs index d6b8b7dae4..7fcaafd20b 100644 --- a/client/doublezero/src/main.rs +++ b/client/doublezero/src/main.rs @@ -28,6 +28,7 @@ use doublezero_cli::{ checkversion::check_version, doublezerocommand::CliCommandImpl, geoclicommand::GeoCliCommandImpl, version::VersionCliCommand, }; +use doublezero_cli_core::LogLevel; use doublezero_sdk::{geolocation::client::GeoClient, DZClient, ProgramVersion}; use doublezero_serviceability::pda::get_globalstate_pda; use servicecontroller::ServiceControllerImpl; @@ -84,13 +85,15 @@ struct App { /// Suppress version warning output #[arg(long, global = true)] no_version_warning: bool, - /// Increase diagnostic logging verbosity. Repeat for higher levels: - /// `--log-verbose` raises to debug, `--log-verbose --log-verbose` to trace. - /// Renamed from `--verbose` to avoid colliding with the per-subcommand - /// `--verbose` flags inherited from earlier releases of `doublezero - /// connect` / `disconnect`. - #[arg(long = "log-verbose", action = clap::ArgAction::Count, global = true)] - log_verbosity: u8, + /// Diagnostic logging level. One of: `off`, `error`, `warn` (default), `info`, `debug`, `trace`. + #[arg( + long = "log-level", + value_name = "LEVEL", + value_enum, + default_value_t = LogLevel::default(), + global = true, + )] + log_level: LogLevel, /// Print version information #[arg(short = 'V', long = "version", action = clap::ArgAction::SetTrue)] version: bool, @@ -104,7 +107,7 @@ async fn main() -> eyre::Result<()> { let app = App::parse(); - doublezero_cli_core::init_logging(app.log_verbosity); + doublezero_cli_core::init_logging(app.log_level); if let Some(sock_file) = &app.sock_file { ServiceControllerImpl::set_global_socket_path(sock_file.to_string_lossy()); diff --git a/crates/doublezero-cli-core/src/lib.rs b/crates/doublezero-cli-core/src/lib.rs index 9dfa0c5047..8eeedbcd5c 100644 --- a/crates/doublezero-cli-core/src/lib.rs +++ b/crates/doublezero-cli-core/src/lib.rs @@ -17,5 +17,5 @@ pub mod validators; pub use context::{CliContext, CliContextBuilder, OutputFormat}; pub use error::{render_error, render_eyre, CliError, Result}; -pub use logging::init_logging; +pub use logging::{init_logging, LogLevel}; pub use requirements::RequirementCheck; diff --git a/crates/doublezero-cli-core/src/logging.rs b/crates/doublezero-cli-core/src/logging.rs index 61a9fdaa17..fe5237c445 100644 --- a/crates/doublezero-cli-core/src/logging.rs +++ b/crates/doublezero-cli-core/src/logging.rs @@ -2,20 +2,47 @@ //! //! Per RFC-20 (§Diagnostic logging): diagnostic output goes to standard //! error through `tracing`. The binary configures the global log level from -//! `--verbose`; modules use the standard log macros (`debug!`, `info!`, +//! `--log-level`; modules use the standard log macros (`debug!`, `info!`, //! `warn!`, `error!`, `trace!`) for anything that explains what a verb is //! doing internally. JSON output on stdout stays parseable because logs go //! to stderr. +use clap::ValueEnum; use tracing_subscriber::EnvFilter; -/// Configure the global `tracing` subscriber from a verbosity count. +/// Diagnostic log level selectable from `--log-level`. /// -/// - `0` → `warn` (default for non-verbose runs) -/// - `1` → `debug` (`-v`) -/// - `2` or more → `trace` (`-vv`) +/// Mirrors the `tracing` level hierarchy plus an explicit `Off` that silences +/// every level. `Warn` is the default for non-verbose runs; `Debug` and +/// `Trace` are the levels operators reach for when chasing a bug. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)] +#[value(rename_all = "lower")] +pub enum LogLevel { + Off, + Error, + #[default] + Warn, + Info, + Debug, + Trace, +} + +impl LogLevel { + fn as_filter_str(self) -> &'static str { + match self { + LogLevel::Off => "off", + LogLevel::Error => "error", + LogLevel::Warn => "warn", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + } + } +} + +/// Configure the global `tracing` subscriber from a `LogLevel`. /// -/// If the `RUST_LOG` environment variable is set, it overrides the verbosity +/// If the `RUST_LOG` environment variable is set, it overrides the level /// argument; this matches what operators expect from standard Rust logging /// stacks and makes it possible to tune per-module log levels from the /// environment without changing CLI flags. @@ -25,16 +52,11 @@ use tracing_subscriber::EnvFilter; /// /// Safe to call multiple times: subsequent calls are no-ops because the /// subscriber registration uses `try_init`. -pub fn init_logging(verbosity: u8) { +pub fn init_logging(level: LogLevel) { let filter = if let Ok(env_filter) = EnvFilter::try_from_default_env() { env_filter } else { - let level = match verbosity { - 0 => "warn", - 1 => "debug", - _ => "trace", - }; - EnvFilter::new(level) + EnvFilter::new(level.as_filter_str()) }; let _ = tracing_subscriber::fmt()