Implement json output for solana ping (#22959)
This commit is contained in:
parent
9548ea61e5
commit
d2c89213ff
|
@ -46,6 +46,8 @@ use {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static CHECK_MARK: Emoji = Emoji("✅ ", "");
|
||||||
|
static CROSS_MARK: Emoji = Emoji("❌ ", "");
|
||||||
static WARNING: Emoji = Emoji("⚠️", "!");
|
static WARNING: Emoji = Emoji("⚠️", "!");
|
||||||
|
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
|
@ -2524,6 +2526,172 @@ impl fmt::Display for CliGossipNodes {
|
||||||
impl QuietDisplay for CliGossipNodes {}
|
impl QuietDisplay for CliGossipNodes {}
|
||||||
impl VerboseDisplay for CliGossipNodes {}
|
impl VerboseDisplay for CliGossipNodes {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CliPing {
|
||||||
|
pub source_pubkey: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub fixed_blockhash: Option<String>,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub blockhash_from_cluster: bool,
|
||||||
|
pub pings: Vec<CliPingData>,
|
||||||
|
pub transaction_stats: CliPingTxStats,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub confirmation_stats: Option<CliPingConfirmationStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CliPing {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln_name_value(f, "Source Account:", &self.source_pubkey)?;
|
||||||
|
if let Some(fixed_blockhash) = &self.fixed_blockhash {
|
||||||
|
let blockhash_origin = if self.blockhash_from_cluster {
|
||||||
|
"fetched from cluster"
|
||||||
|
} else {
|
||||||
|
"supplied from cli arguments"
|
||||||
|
};
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Fixed blockhash is used: {} ({})",
|
||||||
|
fixed_blockhash, blockhash_origin
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
for ping in &self.pings {
|
||||||
|
write!(f, "{}", ping)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "--- transaction statistics ---")?;
|
||||||
|
write!(f, "{}", self.transaction_stats)?;
|
||||||
|
if let Some(confirmation_stats) = &self.confirmation_stats {
|
||||||
|
write!(f, "{}", confirmation_stats)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuietDisplay for CliPing {}
|
||||||
|
impl VerboseDisplay for CliPing {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CliPingData {
|
||||||
|
pub success: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub signature: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub ms: Option<u64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub error: Option<String>,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub print_timestamp: bool,
|
||||||
|
pub timestamp: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub lamports: Option<u64>,
|
||||||
|
}
|
||||||
|
impl fmt::Display for CliPingData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let (mark, msg) = if let Some(signature) = &self.signature {
|
||||||
|
if self.success {
|
||||||
|
(
|
||||||
|
CHECK_MARK,
|
||||||
|
format!(
|
||||||
|
"{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
|
||||||
|
self.lamports.unwrap(),
|
||||||
|
self.sequence,
|
||||||
|
self.ms.unwrap(),
|
||||||
|
signature
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else if let Some(error) = &self.error {
|
||||||
|
(
|
||||||
|
CROSS_MARK,
|
||||||
|
format!(
|
||||||
|
"Transaction failed: seq={:<3} error={:?} signature={}",
|
||||||
|
self.sequence, error, signature
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
CROSS_MARK,
|
||||||
|
format!(
|
||||||
|
"Confirmation timeout: seq={:<3} signature={}",
|
||||||
|
self.sequence, signature
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
CROSS_MARK,
|
||||||
|
format!(
|
||||||
|
"Submit failed: seq={:<3} error={:?}",
|
||||||
|
self.sequence,
|
||||||
|
self.error.as_ref().unwrap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{}{}{}",
|
||||||
|
if self.print_timestamp {
|
||||||
|
&self.timestamp
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
mark,
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuietDisplay for CliPingData {}
|
||||||
|
impl VerboseDisplay for CliPingData {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CliPingTxStats {
|
||||||
|
pub num_transactions: u32,
|
||||||
|
pub num_transaction_confirmed: u32,
|
||||||
|
}
|
||||||
|
impl fmt::Display for CliPingTxStats {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
|
||||||
|
self.num_transactions,
|
||||||
|
self.num_transaction_confirmed,
|
||||||
|
(100.
|
||||||
|
- f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions)
|
||||||
|
* 100.)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuietDisplay for CliPingTxStats {}
|
||||||
|
impl VerboseDisplay for CliPingTxStats {}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CliPingConfirmationStats {
|
||||||
|
pub min: f64,
|
||||||
|
pub mean: f64,
|
||||||
|
pub max: f64,
|
||||||
|
pub std_dev: f64,
|
||||||
|
}
|
||||||
|
impl fmt::Display for CliPingConfirmationStats {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
|
||||||
|
self.min, self.mean, self.max, self.std_dev,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl QuietDisplay for CliPingConfirmationStats {}
|
||||||
|
impl VerboseDisplay for CliPingConfirmationStats {}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use {
|
use {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use {
|
||||||
spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
|
spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
|
||||||
},
|
},
|
||||||
clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand},
|
clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand},
|
||||||
console::{style, Emoji},
|
console::style,
|
||||||
crossbeam_channel::unbounded,
|
crossbeam_channel::unbounded,
|
||||||
serde::{Deserialize, Serialize},
|
serde::{Deserialize, Serialize},
|
||||||
solana_clap_utils::{
|
solana_clap_utils::{
|
||||||
|
@ -16,7 +16,7 @@ use {
|
||||||
solana_cli_output::{
|
solana_cli_output::{
|
||||||
display::{
|
display::{
|
||||||
build_balance_message, format_labeled_address, new_spinner_progress_bar,
|
build_balance_message, format_labeled_address, new_spinner_progress_bar,
|
||||||
println_name_value, println_transaction, unix_timestamp_to_string, writeln_name_value,
|
println_transaction, unix_timestamp_to_string, writeln_name_value,
|
||||||
},
|
},
|
||||||
*,
|
*,
|
||||||
},
|
},
|
||||||
|
@ -75,9 +75,6 @@ use {
|
||||||
thiserror::Error,
|
thiserror::Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
static CHECK_MARK: Emoji = Emoji("✅ ", "");
|
|
||||||
static CROSS_MARK: Emoji = Emoji("❌ ", "");
|
|
||||||
|
|
||||||
pub trait ClusterQuerySubCommands {
|
pub trait ClusterQuerySubCommands {
|
||||||
fn cluster_query_subcommands(self) -> Self;
|
fn cluster_query_subcommands(self) -> Self;
|
||||||
}
|
}
|
||||||
|
@ -1354,15 +1351,14 @@ pub fn process_ping(
|
||||||
fixed_blockhash: &Option<Hash>,
|
fixed_blockhash: &Option<Hash>,
|
||||||
print_timestamp: bool,
|
print_timestamp: bool,
|
||||||
) -> ProcessResult {
|
) -> ProcessResult {
|
||||||
println_name_value("Source Account:", &config.signers[0].pubkey().to_string());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let (signal_sender, signal_receiver) = unbounded();
|
let (signal_sender, signal_receiver) = unbounded();
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
let _ = signal_sender.send(());
|
let _ = signal_sender.send(());
|
||||||
})
|
})
|
||||||
.expect("Error setting Ctrl-C handler");
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
|
let mut cli_pings = vec![];
|
||||||
|
|
||||||
let mut submit_count = 0;
|
let mut submit_count = 0;
|
||||||
let mut confirmed_count = 0;
|
let mut confirmed_count = 0;
|
||||||
let mut confirmation_time: VecDeque<u64> = VecDeque::with_capacity(1024);
|
let mut confirmation_time: VecDeque<u64> = VecDeque::with_capacity(1024);
|
||||||
|
@ -1370,17 +1366,13 @@ pub fn process_ping(
|
||||||
let mut blockhash = rpc_client.get_latest_blockhash()?;
|
let mut blockhash = rpc_client.get_latest_blockhash()?;
|
||||||
let mut lamports = 0;
|
let mut lamports = 0;
|
||||||
let mut blockhash_acquired = Instant::now();
|
let mut blockhash_acquired = Instant::now();
|
||||||
|
let mut blockhash_from_cluster = false;
|
||||||
if let Some(fixed_blockhash) = fixed_blockhash {
|
if let Some(fixed_blockhash) = fixed_blockhash {
|
||||||
let blockhash_origin = if *fixed_blockhash != Hash::default() {
|
if *fixed_blockhash != Hash::default() {
|
||||||
blockhash = *fixed_blockhash;
|
blockhash = *fixed_blockhash;
|
||||||
"supplied from cli arguments"
|
|
||||||
} else {
|
} else {
|
||||||
"fetched from cluster"
|
blockhash_from_cluster = true;
|
||||||
};
|
}
|
||||||
println!(
|
|
||||||
"Fixed blockhash is used: {} ({})",
|
|
||||||
blockhash, blockhash_origin
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
'mainloop: for seq in 0..count.unwrap_or(std::u64::MAX) {
|
'mainloop: for seq in 0..count.unwrap_or(std::u64::MAX) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
@ -1416,11 +1408,7 @@ pub fn process_ping(
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_micros();
|
.as_micros();
|
||||||
if print_timestamp {
|
format!("[{}.{:06}] ", micros / 1_000_000, micros % 1_000_000)
|
||||||
format!("[{}.{:06}] ", micros / 1_000_000, micros % 1_000_000)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match rpc_client.send_transaction(&tx) {
|
match rpc_client.send_transaction(&tx) {
|
||||||
|
@ -1434,35 +1422,51 @@ pub fn process_ping(
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let elapsed_time_millis = elapsed_time.as_millis() as u64;
|
let elapsed_time_millis = elapsed_time.as_millis() as u64;
|
||||||
confirmation_time.push_back(elapsed_time_millis);
|
confirmation_time.push_back(elapsed_time_millis);
|
||||||
println!(
|
let cli_ping_data = CliPingData {
|
||||||
"{}{}{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
|
success: true,
|
||||||
timestamp(),
|
signature: Some(signature.to_string()),
|
||||||
CHECK_MARK, lamports, seq, elapsed_time_millis, signature
|
ms: Some(elapsed_time_millis),
|
||||||
);
|
error: None,
|
||||||
|
timestamp: timestamp(),
|
||||||
|
print_timestamp,
|
||||||
|
sequence: seq,
|
||||||
|
lamports: Some(lamports),
|
||||||
|
};
|
||||||
|
eprint!("{}", cli_ping_data);
|
||||||
|
cli_pings.push(cli_ping_data);
|
||||||
confirmed_count += 1;
|
confirmed_count += 1;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!(
|
let cli_ping_data = CliPingData {
|
||||||
"{}{}Transaction failed: seq={:<3} error={:?} signature={}",
|
success: false,
|
||||||
timestamp(),
|
signature: Some(signature.to_string()),
|
||||||
CROSS_MARK,
|
ms: None,
|
||||||
seq,
|
error: Some(err.to_string()),
|
||||||
err,
|
timestamp: timestamp(),
|
||||||
signature
|
print_timestamp,
|
||||||
);
|
sequence: seq,
|
||||||
|
lamports: None,
|
||||||
|
};
|
||||||
|
eprint!("{}", cli_ping_data);
|
||||||
|
cli_pings.push(cli_ping_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if elapsed_time >= *timeout {
|
if elapsed_time >= *timeout {
|
||||||
println!(
|
let cli_ping_data = CliPingData {
|
||||||
"{}{}Confirmation timeout: seq={:<3} signature={}",
|
success: false,
|
||||||
timestamp(),
|
signature: Some(signature.to_string()),
|
||||||
CROSS_MARK,
|
ms: None,
|
||||||
seq,
|
error: None,
|
||||||
signature
|
timestamp: timestamp(),
|
||||||
);
|
print_timestamp,
|
||||||
|
sequence: seq,
|
||||||
|
lamports: None,
|
||||||
|
};
|
||||||
|
eprint!("{}", cli_ping_data);
|
||||||
|
cli_pings.push(cli_ping_data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1476,13 +1480,18 @@ pub fn process_ping(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!(
|
let cli_ping_data = CliPingData {
|
||||||
"{}{}Submit failed: seq={:<3} error={:?}",
|
success: false,
|
||||||
timestamp(),
|
signature: None,
|
||||||
CROSS_MARK,
|
ms: None,
|
||||||
seq,
|
error: Some(err.to_string()),
|
||||||
err
|
timestamp: timestamp(),
|
||||||
);
|
print_timestamp,
|
||||||
|
sequence: seq,
|
||||||
|
lamports: None,
|
||||||
|
};
|
||||||
|
eprint!("{}", cli_ping_data);
|
||||||
|
cli_pings.push(cli_ping_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
submit_count += 1;
|
submit_count += 1;
|
||||||
|
@ -1492,28 +1501,34 @@ pub fn process_ping(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
let transaction_stats = CliPingTxStats {
|
||||||
println!("--- transaction statistics ---");
|
num_transactions: submit_count,
|
||||||
println!(
|
num_transaction_confirmed: confirmed_count,
|
||||||
"{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
|
};
|
||||||
submit_count,
|
let confirmation_stats = if !confirmation_time.is_empty() {
|
||||||
confirmed_count,
|
|
||||||
(100. - f64::from(confirmed_count) / f64::from(submit_count) * 100.)
|
|
||||||
);
|
|
||||||
if !confirmation_time.is_empty() {
|
|
||||||
let samples: Vec<f64> = confirmation_time.iter().map(|t| *t as f64).collect();
|
let samples: Vec<f64> = confirmation_time.iter().map(|t| *t as f64).collect();
|
||||||
let dist = criterion_stats::Distribution::from(samples.into_boxed_slice());
|
let dist = criterion_stats::Distribution::from(samples.into_boxed_slice());
|
||||||
let mean = dist.mean();
|
let mean = dist.mean();
|
||||||
println!(
|
Some(CliPingConfirmationStats {
|
||||||
"confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
|
min: dist.min(),
|
||||||
dist.min(),
|
|
||||||
mean,
|
mean,
|
||||||
dist.max(),
|
max: dist.max(),
|
||||||
dist.std_dev(Some(mean))
|
std_dev: dist.std_dev(Some(mean)),
|
||||||
);
|
})
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok("".to_string())
|
let cli_ping = CliPing {
|
||||||
|
source_pubkey: config.signers[0].pubkey().to_string(),
|
||||||
|
fixed_blockhash: fixed_blockhash.map(|_| blockhash.to_string()),
|
||||||
|
blockhash_from_cluster,
|
||||||
|
pings: cli_pings,
|
||||||
|
transaction_stats,
|
||||||
|
confirmation_stats,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(config.output_format.formatted_string(&cli_ping))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_logs(
|
pub fn parse_logs(
|
||||||
|
|
Loading…
Reference in New Issue