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
75 changes: 62 additions & 13 deletions src/tools/be/be_http_client.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
use crate::config_loader;
use crate::error::{CliError, Result};
use crate::executor;
use crate::tools::{be, mysql};
use crate::ui;
use std::collections::BTreeSet;
use std::process::Command;

const BE_DEFAULT_IP: &str = "127.0.0.1";

/// Send an HTTP GET request to a BE API endpoint
pub fn request_be_webserver_port(endpoint: &str, filter_pattern: Option<&str>) -> Result<String> {
let be_http_ports = get_be_http_ports()?;
let mut be_targets: BTreeSet<(String, u16)> = BTreeSet::new();

for &port in &be_http_ports {
let url = format!("http://{BE_DEFAULT_IP}:{port}{endpoint}");
let ports = get_be_http_ports()?;

let selected_host = be::list::get_selected_be_host();

let cluster_hosts = get_be_ip().unwrap_or_default();

let mut all_hosts = BTreeSet::new();
if let Some(host) = &selected_host {
all_hosts.insert(host.clone());
}
for host in cluster_hosts {
all_hosts.insert(host);
}

if all_hosts.is_empty() {
all_hosts.insert(BE_DEFAULT_IP.to_string());
}

for host in all_hosts {
be_targets.extend(ports.iter().map(|p| (host.clone(), *p)));
}

for (host, port) in &be_targets {
let url = format!("http://{host}:{port}{endpoint}");
let mut curl_cmd = Command::new("curl");
curl_cmd.args(["-sS", &url]);

Expand All @@ -31,27 +55,52 @@ pub fn request_be_webserver_port(endpoint: &str, filter_pattern: Option<&str>) -
}
}

let ports_str = be_http_ports
let ports_str = be_targets
.iter()
.map(|p| p.to_string())
.map(|(h, p)| format!("{h}:{p}"))
.collect::<Vec<_>>()
.join(", ");

ui::print_warning(
"Could not connect to any BE http endpoint. You can select a host via 'be-list'.",
);
Err(CliError::ToolExecutionFailed(format!(
"Could not connect to any BE http port ({ports_str}). Check if BE is running."
)))
}

/// Get BE HTTP ports from configuration or use defaults
pub fn get_be_http_ports() -> Result<Vec<u16>> {
match config_loader::load_config() {
Ok(doris_config) => Ok(doris_config.get_be_http_ports()),
Err(_) => {
// Fallback to default ports if configuration cannot be loaded
ui::print_warning(
"Could not load configuration, using default BE HTTP ports (8040, 8041)",
);
Ok(vec![8040, 8041])
if let Ok(doris_config) = config_loader::load_config() {
let config_ports = doris_config.get_be_http_ports();
if !config_ports.is_empty() && config_ports != vec![8040, 8041] {
return Ok(config_ports);
}
}

if let Ok(info) = mysql::ClusterInfo::load_from_file() {
let be_ports: Vec<u16> = info
.backends
.iter()
.filter(|b| b.alive)
.map(|b| b.http_port)
.collect();

if !be_ports.is_empty() {
return Ok(be_ports);
}
}

Ok(vec![8040, 8041])
}

pub fn get_be_ip() -> Result<Vec<String>> {
if let Ok(info) = mysql::ClusterInfo::load_from_file() {
let hosts = info.list_be_hosts();
if !hosts.is_empty() {
return Ok(hosts);
}
}

Ok(vec![BE_DEFAULT_IP.to_string()])
}
59 changes: 59 additions & 0 deletions src/tools/be/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use crate::config::Config;
use crate::error::{CliError, Result};
use crate::tools::{ExecutionResult, Tool};
use crate::ui;

pub use crate::tools::common::host_selection::{
get_selected_host as get_selected_be_host_generic,
set_selected_host as set_selected_be_host_generic,
};
pub fn set_selected_be_host(host: String) {
set_selected_be_host_generic(true, host);
}
pub fn get_selected_be_host() -> Option<String> {
get_selected_be_host_generic(true)
}

pub struct BeListTool;

impl Tool for BeListTool {
fn name(&self) -> &str {
"be-list"
}

fn description(&self) -> &str {
"List and select a BE host (IP) for this session"
}

fn requires_pid(&self) -> bool {
false
}

fn execute(&self, _config: &Config, _pid: u32) -> Result<crate::tools::ExecutionResult> {
let info = crate::tools::mysql::ClusterInfo::load_from_file()?;
let hosts = info.list_be_hosts();
if hosts.is_empty() {
return Err(CliError::ConfigError(
"No BE hosts found in clusters.toml".to_string(),
));
}

let items: Vec<String> = hosts;

let selection = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
.with_prompt("Select Backend (BE) host")
.items(&items)
.default(0)
.interact()
.map_err(|e| CliError::InvalidInput(format!("BE selection failed: {e}")))?;

let host = items[selection].clone();
set_selected_be_host(host.clone());
ui::print_success(&format!("Selected BE host: {host}"));

Ok(ExecutionResult {
output_path: std::path::PathBuf::from("console_output"),
message: "BE host updated for this session".to_string(),
})
}
}
2 changes: 2 additions & 0 deletions src/tools/be/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
mod be_http_client;
mod be_vars;
mod jmap;
mod list;
mod memz;
mod pipeline_tasks;
mod pstack;
mod response_handler;

pub use be_vars::BeVarsTool;
pub use jmap::{JmapDumpTool, JmapHistoTool};
pub use list::BeListTool;
pub use memz::{MemzGlobalTool, MemzTool};
pub use pipeline_tasks::PipelineTasksTool;
pub use pstack::PstackTool;
Expand Down
29 changes: 29 additions & 0 deletions src/tools/common/host_selection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use once_cell::sync::OnceCell;
use std::sync::Mutex;

static SELECTED_FE_HOST: OnceCell<Mutex<Option<String>>> = OnceCell::new();
static SELECTED_BE_HOST: OnceCell<Mutex<Option<String>>> = OnceCell::new();

fn storage(cell: &OnceCell<Mutex<Option<String>>>) -> &Mutex<Option<String>> {
cell.get_or_init(|| Mutex::new(None))
}

pub fn set_selected_host(is_be: bool, host: String) {
let cell = if is_be {
&SELECTED_BE_HOST
} else {
&SELECTED_FE_HOST
};
if let Ok(mut guard) = storage(cell).lock() {
*guard = Some(host);
}
}

pub fn get_selected_host(is_be: bool) -> Option<String> {
let cell = if is_be {
&SELECTED_BE_HOST
} else {
&SELECTED_FE_HOST
};
storage(cell).lock().ok().and_then(|g| g.clone())
}
1 change: 1 addition & 0 deletions src/tools/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod format_utils;
pub mod fs_utils;
pub mod host_selection;
pub mod jmap;
53 changes: 53 additions & 0 deletions src/tools/fe/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use crate::config::Config;
use crate::error::{CliError, Result};
use crate::tools::Tool;
use crate::ui;
use std::collections::BTreeSet;

pub struct FeListTool;

impl Tool for FeListTool {
fn name(&self) -> &str {
"fe-list"
}

fn description(&self) -> &str {
"List and select a FE host (IP) for this session"
}

fn requires_pid(&self) -> bool {
false
}

fn execute(&self, _config: &Config, _pid: u32) -> Result<crate::tools::ExecutionResult> {
let info = crate::tools::mysql::ClusterInfo::load_from_file()?;
let mut hosts: BTreeSet<String> = BTreeSet::new();
for fe in info.frontends.iter().filter(|f| f.alive) {
if !fe.host.is_empty() {
hosts.insert(fe.host.clone());
}
}
if hosts.is_empty() {
return Err(CliError::ConfigError(
"No FE hosts found in clusters.toml".to_string(),
));
}
let items: Vec<String> = hosts.iter().cloned().collect();

let selection = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
.with_prompt("Select Frontend (FE) host")
.items(&items)
.default(0)
.interact()
.map_err(|e| CliError::InvalidInput(format!("FE selection failed: {e}")))?;

let host = items[selection].clone();
crate::tools::common::host_selection::set_selected_host(false, host.clone());
ui::print_success(&format!("Selected FE host: {host}"));

Ok(crate::tools::ExecutionResult {
output_path: std::path::PathBuf::from("console_output"),
message: "FE target updated for this session".to_string(),
})
}
}
2 changes: 2 additions & 0 deletions src/tools/fe/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
mod jmap;
mod jstack;
mod list;
mod profiler;
pub mod routine_load;
pub mod table_info;

pub use jmap::{JmapDumpTool, JmapHistoTool};
pub use jstack::JstackTool;
pub use list::FeListTool;
pub use profiler::FeProfilerTool;
pub use routine_load::{RoutineLoadJobLister, get_routine_load_tools};
pub use table_info::{FeTableInfoTool, TableIdentity, TableInfoReport};
6 changes: 3 additions & 3 deletions src/tools/fe/routine_load/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ pub use traffic_monitor::RoutineLoadTrafficMonitor;
/// Routine Load tool index enum to avoid hardcoded indices
#[derive(Debug, Clone, Copy)]
pub enum RoutineLoadToolIndex {
JobLister = 4,
PerformanceAnalyzer = 5,
TrafficMonitor = 6,
JobLister = 5,
PerformanceAnalyzer = 6,
TrafficMonitor = 7,
}

impl RoutineLoadToolIndex {
Expand Down
11 changes: 6 additions & 5 deletions src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ use std::path::PathBuf;
/// Result of executing a tool
#[derive(Debug)]
pub struct ExecutionResult {
/// Path to the generated output file
pub output_path: PathBuf,
/// Success message describing the operation
pub message: String,
}

Expand All @@ -25,7 +23,6 @@ pub trait Tool {
fn execute(&self, config: &Config, pid: u32) -> Result<ExecutionResult>;

/// Indicates whether the tool requires a process PID to execute.
/// Most tools do, so the default is true.
fn requires_pid(&self) -> bool {
true
}
Expand All @@ -47,18 +44,21 @@ impl ToolRegistry {
/// Creates a new tool registry with all available tools
pub fn new() -> Self {
use crate::tools::be::{
BeVarsTool, MemzGlobalTool, MemzTool, PipelineTasksTool, PstackTool,
BeListTool, BeVarsTool, MemzGlobalTool, MemzTool, PipelineTasksTool, PstackTool,
};
use crate::tools::be::{JmapDumpTool as BeJmapDumpTool, JmapHistoTool as BeJmapHistoTool};
use crate::tools::fe::routine_load::get_routine_load_tools;
use crate::tools::fe::{FeProfilerTool, JmapDumpTool, JmapHistoTool, JstackTool};
use crate::tools::fe::{
FeListTool, FeProfilerTool, JmapDumpTool, JmapHistoTool, JstackTool,
};

let mut registry = Self {
fe_tools: Vec::new(),
be_tools: Vec::new(),
};

// Register FE tools
registry.fe_tools.push(Box::new(FeListTool));
registry.fe_tools.push(Box::new(JmapDumpTool));
registry.fe_tools.push(Box::new(JmapHistoTool));
registry.fe_tools.push(Box::new(JstackTool));
Expand All @@ -68,6 +68,7 @@ impl ToolRegistry {
registry.fe_tools.extend(get_routine_load_tools());

// Register BE tools
registry.be_tools.push(Box::new(BeListTool));
registry.be_tools.push(Box::new(PstackTool));
registry.be_tools.push(Box::new(BeVarsTool));
registry.be_tools.push(Box::new(BeJmapDumpTool));
Expand Down
19 changes: 18 additions & 1 deletion src/tools/mysql/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ impl Backend {
}

/// Parse Tag information and extract cloud cluster information
/// This is a private helper method specifically for Backend Tag field parsing
fn parse_tag_info(tag_str: &str) -> Option<String> {
if tag_str.is_empty() || tag_str == "{}" {
return None;
Expand Down Expand Up @@ -192,6 +191,24 @@ pub struct ClusterInfo {
}

impl ClusterInfo {
pub fn load_from_file() -> Result<Self> {
let config_dir = fs_utils::get_user_config_dir()?;
let file_path = config_dir.join("clusters.toml");
let content = fs_utils::read_file_content(&file_path)?;
let info: ClusterInfo = toml::from_str(&content).map_err(|e| {
crate::error::CliError::ConfigError(format!("Failed to parse clusters.toml: {e}"))
})?;
Ok(info)
}

pub fn list_be_hosts(&self) -> Vec<String> {
self.backends
.iter()
.filter(|b| b.alive)
.map(|b| b.host.clone())
.collect()
}

pub fn save_to_file(&self) -> Result<PathBuf> {
self.validate()?;
let config_dir = fs_utils::get_user_config_dir()?;
Expand Down
Loading
Loading