Skip to content

Commit 8b67caa

Browse files
authored
feat: add BE and FE host selection tools and enhance HTTP client functionality (#20)
1 parent d2e818d commit 8b67caa

File tree

12 files changed

+266
-34
lines changed

12 files changed

+266
-34
lines changed

src/tools/be/be_http_client.rs

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
use crate::config_loader;
22
use crate::error::{CliError, Result};
33
use crate::executor;
4+
use crate::tools::{be, mysql};
45
use crate::ui;
6+
use std::collections::BTreeSet;
57
use std::process::Command;
68

79
const BE_DEFAULT_IP: &str = "127.0.0.1";
810

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

13-
for &port in &be_http_ports {
14-
let url = format!("http://{BE_DEFAULT_IP}:{port}{endpoint}");
15+
let ports = get_be_http_ports()?;
16+
17+
let selected_host = be::list::get_selected_be_host();
18+
19+
let cluster_hosts = get_be_ip().unwrap_or_default();
20+
21+
let mut all_hosts = BTreeSet::new();
22+
if let Some(host) = &selected_host {
23+
all_hosts.insert(host.clone());
24+
}
25+
for host in cluster_hosts {
26+
all_hosts.insert(host);
27+
}
28+
29+
if all_hosts.is_empty() {
30+
all_hosts.insert(BE_DEFAULT_IP.to_string());
31+
}
32+
33+
for host in all_hosts {
34+
be_targets.extend(ports.iter().map(|p| (host.clone(), *p)));
35+
}
36+
37+
for (host, port) in &be_targets {
38+
let url = format!("http://{host}:{port}{endpoint}");
1539
let mut curl_cmd = Command::new("curl");
1640
curl_cmd.args(["-sS", &url]);
1741

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

34-
let ports_str = be_http_ports
58+
let ports_str = be_targets
3559
.iter()
36-
.map(|p| p.to_string())
60+
.map(|(h, p)| format!("{h}:{p}"))
3761
.collect::<Vec<_>>()
3862
.join(", ");
3963

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

4572
/// Get BE HTTP ports from configuration or use defaults
4673
pub fn get_be_http_ports() -> Result<Vec<u16>> {
47-
match config_loader::load_config() {
48-
Ok(doris_config) => Ok(doris_config.get_be_http_ports()),
49-
Err(_) => {
50-
// Fallback to default ports if configuration cannot be loaded
51-
ui::print_warning(
52-
"Could not load configuration, using default BE HTTP ports (8040, 8041)",
53-
);
54-
Ok(vec![8040, 8041])
74+
if let Ok(doris_config) = config_loader::load_config() {
75+
let config_ports = doris_config.get_be_http_ports();
76+
if !config_ports.is_empty() && config_ports != vec![8040, 8041] {
77+
return Ok(config_ports);
5578
}
5679
}
80+
81+
if let Ok(info) = mysql::ClusterInfo::load_from_file() {
82+
let be_ports: Vec<u16> = info
83+
.backends
84+
.iter()
85+
.filter(|b| b.alive)
86+
.map(|b| b.http_port)
87+
.collect();
88+
89+
if !be_ports.is_empty() {
90+
return Ok(be_ports);
91+
}
92+
}
93+
94+
Ok(vec![8040, 8041])
95+
}
96+
97+
pub fn get_be_ip() -> Result<Vec<String>> {
98+
if let Ok(info) = mysql::ClusterInfo::load_from_file() {
99+
let hosts = info.list_be_hosts();
100+
if !hosts.is_empty() {
101+
return Ok(hosts);
102+
}
103+
}
104+
105+
Ok(vec![BE_DEFAULT_IP.to_string()])
57106
}

src/tools/be/list.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use crate::config::Config;
2+
use crate::error::{CliError, Result};
3+
use crate::tools::{ExecutionResult, Tool};
4+
use crate::ui;
5+
6+
pub use crate::tools::common::host_selection::{
7+
get_selected_host as get_selected_be_host_generic,
8+
set_selected_host as set_selected_be_host_generic,
9+
};
10+
pub fn set_selected_be_host(host: String) {
11+
set_selected_be_host_generic(true, host);
12+
}
13+
pub fn get_selected_be_host() -> Option<String> {
14+
get_selected_be_host_generic(true)
15+
}
16+
17+
pub struct BeListTool;
18+
19+
impl Tool for BeListTool {
20+
fn name(&self) -> &str {
21+
"be-list"
22+
}
23+
24+
fn description(&self) -> &str {
25+
"List and select a BE host (IP) for this session"
26+
}
27+
28+
fn requires_pid(&self) -> bool {
29+
false
30+
}
31+
32+
fn execute(&self, _config: &Config, _pid: u32) -> Result<crate::tools::ExecutionResult> {
33+
let info = crate::tools::mysql::ClusterInfo::load_from_file()?;
34+
let hosts = info.list_be_hosts();
35+
if hosts.is_empty() {
36+
return Err(CliError::ConfigError(
37+
"No BE hosts found in clusters.toml".to_string(),
38+
));
39+
}
40+
41+
let items: Vec<String> = hosts;
42+
43+
let selection = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
44+
.with_prompt("Select Backend (BE) host")
45+
.items(&items)
46+
.default(0)
47+
.interact()
48+
.map_err(|e| CliError::InvalidInput(format!("BE selection failed: {e}")))?;
49+
50+
let host = items[selection].clone();
51+
set_selected_be_host(host.clone());
52+
ui::print_success(&format!("Selected BE host: {host}"));
53+
54+
Ok(ExecutionResult {
55+
output_path: std::path::PathBuf::from("console_output"),
56+
message: "BE host updated for this session".to_string(),
57+
})
58+
}
59+
}

src/tools/be/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
mod be_http_client;
22
mod be_vars;
33
mod jmap;
4+
mod list;
45
mod memz;
56
mod pipeline_tasks;
67
mod pstack;
78
mod response_handler;
89

910
pub use be_vars::BeVarsTool;
1011
pub use jmap::{JmapDumpTool, JmapHistoTool};
12+
pub use list::BeListTool;
1113
pub use memz::{MemzGlobalTool, MemzTool};
1214
pub use pipeline_tasks::PipelineTasksTool;
1315
pub use pstack::PstackTool;

src/tools/common/host_selection.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use once_cell::sync::OnceCell;
2+
use std::sync::Mutex;
3+
4+
static SELECTED_FE_HOST: OnceCell<Mutex<Option<String>>> = OnceCell::new();
5+
static SELECTED_BE_HOST: OnceCell<Mutex<Option<String>>> = OnceCell::new();
6+
7+
fn storage(cell: &OnceCell<Mutex<Option<String>>>) -> &Mutex<Option<String>> {
8+
cell.get_or_init(|| Mutex::new(None))
9+
}
10+
11+
pub fn set_selected_host(is_be: bool, host: String) {
12+
let cell = if is_be {
13+
&SELECTED_BE_HOST
14+
} else {
15+
&SELECTED_FE_HOST
16+
};
17+
if let Ok(mut guard) = storage(cell).lock() {
18+
*guard = Some(host);
19+
}
20+
}
21+
22+
pub fn get_selected_host(is_be: bool) -> Option<String> {
23+
let cell = if is_be {
24+
&SELECTED_BE_HOST
25+
} else {
26+
&SELECTED_FE_HOST
27+
};
28+
storage(cell).lock().ok().and_then(|g| g.clone())
29+
}

src/tools/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod format_utils;
22
pub mod fs_utils;
3+
pub mod host_selection;
34
pub mod jmap;

src/tools/fe/list.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use crate::config::Config;
2+
use crate::error::{CliError, Result};
3+
use crate::tools::Tool;
4+
use crate::ui;
5+
use std::collections::BTreeSet;
6+
7+
pub struct FeListTool;
8+
9+
impl Tool for FeListTool {
10+
fn name(&self) -> &str {
11+
"fe-list"
12+
}
13+
14+
fn description(&self) -> &str {
15+
"List and select a FE host (IP) for this session"
16+
}
17+
18+
fn requires_pid(&self) -> bool {
19+
false
20+
}
21+
22+
fn execute(&self, _config: &Config, _pid: u32) -> Result<crate::tools::ExecutionResult> {
23+
let info = crate::tools::mysql::ClusterInfo::load_from_file()?;
24+
let mut hosts: BTreeSet<String> = BTreeSet::new();
25+
for fe in info.frontends.iter().filter(|f| f.alive) {
26+
if !fe.host.is_empty() {
27+
hosts.insert(fe.host.clone());
28+
}
29+
}
30+
if hosts.is_empty() {
31+
return Err(CliError::ConfigError(
32+
"No FE hosts found in clusters.toml".to_string(),
33+
));
34+
}
35+
let items: Vec<String> = hosts.iter().cloned().collect();
36+
37+
let selection = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
38+
.with_prompt("Select Frontend (FE) host")
39+
.items(&items)
40+
.default(0)
41+
.interact()
42+
.map_err(|e| CliError::InvalidInput(format!("FE selection failed: {e}")))?;
43+
44+
let host = items[selection].clone();
45+
crate::tools::common::host_selection::set_selected_host(false, host.clone());
46+
ui::print_success(&format!("Selected FE host: {host}"));
47+
48+
Ok(crate::tools::ExecutionResult {
49+
output_path: std::path::PathBuf::from("console_output"),
50+
message: "FE target updated for this session".to_string(),
51+
})
52+
}
53+
}

src/tools/fe/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
mod jmap;
22
mod jstack;
3+
mod list;
34
mod profiler;
45
pub mod routine_load;
56
pub mod table_info;
67

78
pub use jmap::{JmapDumpTool, JmapHistoTool};
89
pub use jstack::JstackTool;
10+
pub use list::FeListTool;
911
pub use profiler::FeProfilerTool;
1012
pub use routine_load::{RoutineLoadJobLister, get_routine_load_tools};
1113
pub use table_info::{FeTableInfoTool, TableIdentity, TableInfoReport};

src/tools/fe/routine_load/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ pub use traffic_monitor::RoutineLoadTrafficMonitor;
1818
/// Routine Load tool index enum to avoid hardcoded indices
1919
#[derive(Debug, Clone, Copy)]
2020
pub enum RoutineLoadToolIndex {
21-
JobLister = 4,
22-
PerformanceAnalyzer = 5,
23-
TrafficMonitor = 6,
21+
JobLister = 5,
22+
PerformanceAnalyzer = 6,
23+
TrafficMonitor = 7,
2424
}
2525

2626
impl RoutineLoadToolIndex {

src/tools/mod.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ use std::path::PathBuf;
1010
/// Result of executing a tool
1111
#[derive(Debug)]
1212
pub struct ExecutionResult {
13-
/// Path to the generated output file
1413
pub output_path: PathBuf,
15-
/// Success message describing the operation
1614
pub message: String,
1715
}
1816

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

2725
/// Indicates whether the tool requires a process PID to execute.
28-
/// Most tools do, so the default is true.
2926
fn requires_pid(&self) -> bool {
3027
true
3128
}
@@ -47,18 +44,21 @@ impl ToolRegistry {
4744
/// Creates a new tool registry with all available tools
4845
pub fn new() -> Self {
4946
use crate::tools::be::{
50-
BeVarsTool, MemzGlobalTool, MemzTool, PipelineTasksTool, PstackTool,
47+
BeListTool, BeVarsTool, MemzGlobalTool, MemzTool, PipelineTasksTool, PstackTool,
5148
};
5249
use crate::tools::be::{JmapDumpTool as BeJmapDumpTool, JmapHistoTool as BeJmapHistoTool};
5350
use crate::tools::fe::routine_load::get_routine_load_tools;
54-
use crate::tools::fe::{FeProfilerTool, JmapDumpTool, JmapHistoTool, JstackTool};
51+
use crate::tools::fe::{
52+
FeListTool, FeProfilerTool, JmapDumpTool, JmapHistoTool, JstackTool,
53+
};
5554

5655
let mut registry = Self {
5756
fe_tools: Vec::new(),
5857
be_tools: Vec::new(),
5958
};
6059

6160
// Register FE tools
61+
registry.fe_tools.push(Box::new(FeListTool));
6262
registry.fe_tools.push(Box::new(JmapDumpTool));
6363
registry.fe_tools.push(Box::new(JmapHistoTool));
6464
registry.fe_tools.push(Box::new(JstackTool));
@@ -68,6 +68,7 @@ impl ToolRegistry {
6868
registry.fe_tools.extend(get_routine_load_tools());
6969

7070
// Register BE tools
71+
registry.be_tools.push(Box::new(BeListTool));
7172
registry.be_tools.push(Box::new(PstackTool));
7273
registry.be_tools.push(Box::new(BeVarsTool));
7374
registry.be_tools.push(Box::new(BeJmapDumpTool));

src/tools/mysql/cluster.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@ impl Backend {
138138
}
139139

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

194193
impl ClusterInfo {
194+
pub fn load_from_file() -> Result<Self> {
195+
let config_dir = fs_utils::get_user_config_dir()?;
196+
let file_path = config_dir.join("clusters.toml");
197+
let content = fs_utils::read_file_content(&file_path)?;
198+
let info: ClusterInfo = toml::from_str(&content).map_err(|e| {
199+
crate::error::CliError::ConfigError(format!("Failed to parse clusters.toml: {e}"))
200+
})?;
201+
Ok(info)
202+
}
203+
204+
pub fn list_be_hosts(&self) -> Vec<String> {
205+
self.backends
206+
.iter()
207+
.filter(|b| b.alive)
208+
.map(|b| b.host.clone())
209+
.collect()
210+
}
211+
195212
pub fn save_to_file(&self) -> Result<PathBuf> {
196213
self.validate()?;
197214
let config_dir = fs_utils::get_user_config_dir()?;

0 commit comments

Comments
 (0)