Skip to content

feat(zkstack_cli): Add status page #3036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2e57200
Implement status::run
matias-gonz Oct 7, 2024
16be3fb
Add StatusResponse
matias-gonz Oct 8, 2024
f2272f4
Improve print_status
matias-gonz Oct 8, 2024
1dd1995
Improve System Status
matias-gonz Oct 8, 2024
aa70bbd
Remove ports
matias-gonz Oct 8, 2024
2c8cca3
Merge branch 'main' of github.com:matter-labs/zksync-era into matias/…
matias-gonz Oct 9, 2024
6dca0cf
feat(zkstack_cli): Add component boxes to status page (#3047)
matias-gonz Oct 9, 2024
ee8cd20
Merge branch 'main' into matias/add-status-page
matias-gonz Oct 10, 2024
5f304bb
Merge branch 'main' into matias/add-status-page
matias-gonz Oct 10, 2024
7890cb5
Merge branch 'main' into matias/add-status-page
Deniallugo Oct 14, 2024
f9b53e6
Merge branch 'main' of github.com:matter-labs/zksync-era into matias/…
matias-gonz Oct 14, 2024
1d14cb3
Replace curl wioth reqwest
matias-gonz Oct 14, 2024
3c7c44b
Refactor ports
matias-gonz Oct 14, 2024
cbc6541
Add port boxes
matias-gonz Oct 14, 2024
a5253a0
Add IN USE tag for ports
matias-gonz Oct 14, 2024
06b34eb
Fix is_port_in_use
matias-gonz Oct 14, 2024
4b90f8d
Rename in_use to open
matias-gonz Oct 15, 2024
3bd9e88
Merge branch 'main' of github.com:matter-labs/zksync-era into matias/…
matias-gonz Oct 15, 2024
0d89c8e
Make ports a subcommand
matias-gonz Oct 16, 2024
d0bc5c3
Add draw module
matias-gonz Oct 16, 2024
cc25a45
Add args module
matias-gonz Oct 16, 2024
9993d27
extract msgs
matias-gonz Oct 16, 2024
b4c61b0
Merge branch 'main' into matias/add-status-page
matias-gonz Oct 17, 2024
f687b56
Add ports title
matias-gonz Oct 17, 2024
d17b24c
Merge branch 'matias/add-status-page' of github.com:matter-labs/zksyn…
matias-gonz Oct 17, 2024
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
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
Loading