Skip to content

Commit

Permalink
feat(zkstack_cli): Add status page (#3036)
Browse files Browse the repository at this point in the history
## What ❔

Sample:


![image](https://github.com/user-attachments/assets/712ee40f-c650-43bf-a69c-ba03df37e07a)

---------

Co-authored-by: Danil <[email protected]>
  • Loading branch information
matias-gonz and Deniallugo authored Oct 18, 2024
1 parent 9d88373 commit dd4b7cc
Show file tree
Hide file tree
Showing 9 changed files with 415 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pub mod prover;
pub mod send_transactions;
pub mod snapshot;
pub(crate) mod sql_fmt;
pub mod status;
pub mod test;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use anyhow::Context;
use clap::Parser;
use config::EcosystemConfig;
use xshell::Shell;

use crate::{
commands::dev::messages::{
MSG_API_CONFIG_NOT_FOUND_ERR, MSG_STATUS_PORTS_HELP, MSG_STATUS_URL_HELP,
},
messages::MSG_CHAIN_NOT_FOUND_ERR,
};

#[derive(Debug, Parser)]
pub enum StatusSubcommands {
#[clap(about = MSG_STATUS_PORTS_HELP)]
Ports,
}

#[derive(Debug, Parser)]
pub struct StatusArgs {
#[clap(long, short = 'u', help = MSG_STATUS_URL_HELP)]
pub url: Option<String>,
#[clap(subcommand)]
pub subcommand: Option<StatusSubcommands>,
}

impl StatusArgs {
pub fn get_url(&self, shell: &Shell) -> anyhow::Result<String> {
if let Some(url) = &self.url {
Ok(url.clone())
} else {
let ecosystem = EcosystemConfig::from_file(shell)?;
let chain = ecosystem
.load_current_chain()
.context(MSG_CHAIN_NOT_FOUND_ERR)?;
let general_config = chain.get_general_config()?;
let health_check_port = general_config
.api_config
.context(MSG_API_CONFIG_NOT_FOUND_ERR)?
.healthcheck
.port;
Ok(format!("http://localhost:{}/health", health_check_port))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::{commands::dev::commands::status::utils::is_port_open, utils::ports::PortInfo};

const DEFAULT_LINE_WIDTH: usize = 32;

pub struct BoxProperties {
longest_line: usize,
border: String,
boxed_msg: Vec<String>,
}

impl BoxProperties {
fn new(msg: &str) -> Self {
let longest_line = msg
.lines()
.map(|line| line.len())
.max()
.unwrap_or(0)
.max(DEFAULT_LINE_WIDTH);
let width = longest_line + 2;
let border = "─".repeat(width);
let boxed_msg = msg
.lines()
.map(|line| format!("│ {:longest_line$} │", line))
.collect();
Self {
longest_line,
border,
boxed_msg,
}
}
}

fn single_bordered_box(msg: &str) -> String {
let properties = BoxProperties::new(msg);
format!(
"┌{}┐\n{}\n└{}┘\n",
properties.border,
properties.boxed_msg.join("\n"),
properties.border
)
}

pub fn bordered_boxes(msg1: &str, msg2: Option<&String>) -> String {
if msg2.is_none() {
return single_bordered_box(msg1);
}

let properties1 = BoxProperties::new(msg1);
let properties2 = BoxProperties::new(msg2.unwrap());

let max_lines = properties1.boxed_msg.len().max(properties2.boxed_msg.len());
let header = format!("┌{}┐ ┌{}┐\n", properties1.border, properties2.border);
let footer = format!("└{}┘ └{}┘\n", properties1.border, properties2.border);

let empty_line1 = format!(
"│ {:longest_line$} │",
"",
longest_line = properties1.longest_line
);
let empty_line2 = format!(
"│ {:longest_line$} │",
"",
longest_line = properties2.longest_line
);

let boxed_info: Vec<String> = (0..max_lines)
.map(|i| {
let line1 = properties1.boxed_msg.get(i).unwrap_or(&empty_line1);
let line2 = properties2.boxed_msg.get(i).unwrap_or(&empty_line2);
format!("{} {}", line1, line2)
})
.collect();

format!("{}{}\n{}", header, boxed_info.join("\n"), footer)
}

pub fn format_port_info(port_info: &PortInfo) -> String {
let in_use_tag = if is_port_open(port_info.port) {
" [OPEN]"
} else {
""
};

format!(
" - {}{} > {}\n",
port_info.port, in_use_tag, port_info.description
)
}
135 changes: 135 additions & 0 deletions zkstack_cli/crates/zkstack/src/commands/dev/commands/status/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use std::collections::HashMap;

use anyhow::Context;
use args::{StatusArgs, StatusSubcommands};
use common::logger;
use draw::{bordered_boxes, format_port_info};
use serde::Deserialize;
use serde_json::Value;
use utils::deslugify;
use xshell::Shell;

use crate::{
commands::dev::messages::{
msg_failed_parse_response, msg_not_ready_components, msg_system_status,
MSG_ALL_COMPONENTS_READY, MSG_COMPONENTS, MSG_SOME_COMPONENTS_NOT_READY,
},
utils::ports::EcosystemPortsScanner,
};

pub mod args;
mod draw;
mod utils;

const STATUS_READY: &str = "ready";

#[derive(Deserialize, Debug)]
struct StatusResponse {
status: String,
components: HashMap<String, Component>,
}

#[derive(Deserialize, Debug)]
struct Component {
status: String,
details: Option<Value>,
}

fn print_status(health_check_url: String) -> anyhow::Result<()> {
let client = reqwest::blocking::Client::new();
let response = client.get(&health_check_url).send()?.text()?;

let status_response: StatusResponse =
serde_json::from_str(&response).context(msg_failed_parse_response(&response))?;

if status_response.status.to_lowercase() == STATUS_READY {
logger::success(msg_system_status(&status_response.status));
} else {
logger::warn(msg_system_status(&status_response.status));
}

let mut components_info = String::from(MSG_COMPONENTS);
let mut components = Vec::new();
let mut not_ready_components = Vec::new();

for (component_name, component) in status_response.components {
let readable_name = deslugify(&component_name);
let mut component_info = format!("{}:\n - Status: {}", readable_name, component.status);

if let Some(details) = &component.details {
for (key, value) in details.as_object().unwrap() {
component_info.push_str(&format!("\n - {}: {}", deslugify(key), value));
}
}

if component.status.to_lowercase() != STATUS_READY {
not_ready_components.push(readable_name);
}

components.push(component_info);
}

components.sort_by(|a, b| {
a.lines()
.count()
.cmp(&b.lines().count())
.then_with(|| a.cmp(b))
});

for chunk in components.chunks(2) {
components_info.push_str(&bordered_boxes(&chunk[0], chunk.get(1)));
}

logger::info(components_info);

if not_ready_components.is_empty() {
logger::outro(MSG_ALL_COMPONENTS_READY);
} else {
logger::warn(MSG_SOME_COMPONENTS_NOT_READY);
logger::outro(msg_not_ready_components(&not_ready_components.join(", ")));
}

Ok(())
}

fn print_ports(shell: &Shell) -> anyhow::Result<()> {
let ports = EcosystemPortsScanner::scan(shell)?;
let grouped_ports = ports.group_by_file_path();

let mut all_port_lines: Vec<String> = Vec::new();

for (file_path, port_infos) in grouped_ports {
let mut port_info_lines = String::new();

for port_info in port_infos {
port_info_lines.push_str(&format_port_info(&port_info));
}

all_port_lines.push(format!("{}:\n{}", file_path, port_info_lines));
}

all_port_lines.sort_by(|a, b| {
b.lines()
.count()
.cmp(&a.lines().count())
.then_with(|| a.cmp(b))
});

let mut components_info = String::from("Ports:\n");
for chunk in all_port_lines.chunks(2) {
components_info.push_str(&bordered_boxes(&chunk[0], chunk.get(1)));
}

logger::info(components_info);
Ok(())
}

pub async fn run(shell: &Shell, args: StatusArgs) -> anyhow::Result<()> {
if let Some(StatusSubcommands::Ports) = args.subcommand {
return print_ports(shell);
}

let health_check_url = args.get_url(shell)?;

print_status(health_check_url)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::net::TcpListener;

pub fn is_port_open(port: u16) -> bool {
TcpListener::bind(("0.0.0.0", port)).is_err() || TcpListener::bind(("127.0.0.1", port)).is_err()
}

pub fn deslugify(name: &str) -> String {
name.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
Some(first) => {
let capitalized = first.to_uppercase().collect::<String>() + chars.as_str();
match capitalized.as_str() {
"Http" => "HTTP".to_string(),
"Api" => "API".to_string(),
"Ws" => "WS".to_string(),
_ => capitalized,
}
}
None => String::new(),
}
})
.collect::<Vec<String>>()
.join(" ")
}
23 changes: 23 additions & 0 deletions zkstack_cli/crates/zkstack/src/commands/dev/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,28 @@ pub(super) const MSG_UNABLE_TO_READ_PARSE_JSON_ERR: &str = "Unable to parse JSON
pub(super) const MSG_FAILED_TO_SEND_TXN_ERR: &str = "Failed to send transaction";
pub(super) const MSG_INVALID_L1_RPC_URL_ERR: &str = "Invalid L1 RPC URL";

// Status related messages
pub(super) const MSG_STATUS_ABOUT: &str = "Get status of the server";
pub(super) const MSG_API_CONFIG_NOT_FOUND_ERR: &str = "API config not found";
pub(super) const MSG_STATUS_URL_HELP: &str = "URL of the health check endpoint";
pub(super) const MSG_STATUS_PORTS_HELP: &str = "Show used ports";
pub(super) const MSG_COMPONENTS: &str = "Components:\n";
pub(super) const MSG_ALL_COMPONENTS_READY: &str =
"Overall System Status: All components operational and ready.";
pub(super) const MSG_SOME_COMPONENTS_NOT_READY: &str =
"Overall System Status: Some components are not ready.";

pub(super) fn msg_system_status(status: &str) -> String {
format!("System Status: {}\n", status)
}

pub(super) fn msg_failed_parse_response(response: &str) -> String {
format!("Failed to parse response: {}", response)
}

pub(super) fn msg_not_ready_components(components: &str) -> String {
format!("Not Ready Components: {}", components)
}

// Genesis
pub(super) const MSG_GENESIS_FILE_GENERATION_STARTED: &str = "Regenerate genesis file";
5 changes: 5 additions & 0 deletions zkstack_cli/crates/zkstack/src/commands/dev/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use clap::Subcommand;
use commands::status::args::StatusArgs;
use messages::MSG_STATUS_ABOUT;
use xshell::Shell;

use self::commands::{
Expand Down Expand Up @@ -41,6 +43,8 @@ pub enum DevCommands {
ConfigWriter(ConfigWriterArgs),
#[command(about = MSG_SEND_TXNS_ABOUT)]
SendTransactions(SendTransactionsArgs),
#[command(about = MSG_STATUS_ABOUT)]
Status(StatusArgs),
#[command(about = MSG_GENERATE_GENESIS_ABOUT, alias = "genesis")]
GenerateGenesis,
}
Expand All @@ -59,6 +63,7 @@ pub async fn run(shell: &Shell, args: DevCommands) -> anyhow::Result<()> {
DevCommands::SendTransactions(args) => {
commands::send_transactions::run(shell, args).await?
}
DevCommands::Status(args) => commands::status::run(shell, args).await?,
DevCommands::GenerateGenesis => commands::genesis::run(shell).await?,
}
Ok(())
Expand Down
2 changes: 1 addition & 1 deletion zkstack_cli/crates/zkstack/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ pub enum InceptionSubcommands {
/// Run block-explorer
#[command(subcommand)]
Explorer(ExplorerCommands),
/// Update ZKsync
#[command(subcommand)]
Consensus(consensus::Command),
/// Update ZKsync
#[command(alias = "u")]
Update(UpdateArgs),
#[command(hide = true)]
Expand Down
Loading

0 comments on commit dd4b7cc

Please sign in to comment.