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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SOLANA_RPC_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-level <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

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions client/doublezero/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
95 changes: 92 additions & 3 deletions client/doublezero/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,15 +41,29 @@ use servicecontroller::ServiceControllerImpl;
struct App {
#[command(subcommand)]
command: Option<Command>,
/// 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<String>,
/// DZ ledger RPC URL
#[arg(long, value_name = "RPC_URL", global = true)]
url: Option<String>,
/// DZ ledger WebSocket URL
#[arg(long, value_name = "WEBSOCKET_URL", global = true)]
ws: Option<String>,
/// Solana L1 RPC URL override (does not affect the DZ ledger)
#[arg(long, value_name = "SOLANA_RPC_URL", global = true)]
solana_url: Option<String>,
/// DZ program ID (testnet or devnet)
#[arg(long, value_name = "PROGRAM_ID", global = true)]
program_id: Option<String>,
Expand All @@ -70,6 +85,15 @@ struct App {
/// Suppress version warning output
#[arg(long, global = true)]
no_version_warning: bool,
/// 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,
Expand All @@ -83,12 +107,14 @@ async fn main() -> eyre::Result<()> {

let app = App::parse();

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());
}

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 {
Expand Down Expand Up @@ -428,3 +454,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");
}
}
2 changes: 1 addition & 1 deletion crates/doublezero-cli-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
48 changes: 35 additions & 13 deletions crates/doublezero-cli-core/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand Down
Loading