Skip to content

Commit 88676e8

Browse files
committed
feat(enterprise): add diagnostics commands for system health monitoring
- Add get, update, run, list-checks, last-report, get-report, list-reports subcommands - Support filtering diagnostic runs by checks, nodes, and databases - Integrate with existing output formatting and JMESPath query support - Direct JSON API usage for flexible diagnostic request building - Test coverage against Docker compose environment Implements #163
1 parent 8b51189 commit 88676e8

File tree

4 files changed

+251
-0
lines changed

4 files changed

+251
-0
lines changed

crates/redisctl/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,10 @@ pub enum EnterpriseCommands {
967967
#[command(subcommand)]
968968
Database(EnterpriseDatabaseCommands),
969969

970+
/// Diagnostics operations
971+
#[command(subcommand)]
972+
Diagnostics(crate::commands::enterprise::diagnostics::DiagnosticsCommands),
973+
970974
/// Node operations
971975
#[command(subcommand)]
972976
Node(EnterpriseNodeCommands),
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use anyhow::Context;
2+
use clap::Subcommand;
3+
use redis_enterprise::DiagnosticsHandler;
4+
5+
use crate::cli::OutputFormat;
6+
use crate::connection::ConnectionManager;
7+
use crate::error::Result as CliResult;
8+
9+
#[derive(Debug, Clone, Subcommand)]
10+
pub enum DiagnosticsCommands {
11+
/// Get diagnostics configuration
12+
Get,
13+
14+
/// Update diagnostics configuration
15+
Update {
16+
/// JSON data for configuration update (use @filename or - for stdin)
17+
#[arg(short, long)]
18+
data: String,
19+
},
20+
21+
/// Run diagnostic checks
22+
Run {
23+
/// Specific diagnostic checks to run (comma-separated)
24+
#[arg(long)]
25+
checks: Option<String>,
26+
27+
/// Node UIDs to run diagnostics on (comma-separated)
28+
#[arg(long)]
29+
nodes: Option<String>,
30+
31+
/// Database UIDs to run diagnostics on (comma-separated)
32+
#[arg(long)]
33+
databases: Option<String>,
34+
},
35+
36+
/// List available diagnostic checks
37+
#[command(name = "list-checks")]
38+
ListChecks,
39+
40+
/// Get the last diagnostic report
41+
#[command(name = "last-report")]
42+
LastReport,
43+
44+
/// Get a specific diagnostic report by ID
45+
#[command(name = "get-report")]
46+
GetReport {
47+
/// Report ID
48+
report_id: String,
49+
},
50+
51+
/// List all diagnostic reports
52+
#[command(name = "list-reports")]
53+
ListReports,
54+
}
55+
56+
impl DiagnosticsCommands {
57+
pub async fn execute(
58+
&self,
59+
conn_mgr: &ConnectionManager,
60+
profile_name: Option<&str>,
61+
output_format: OutputFormat,
62+
query: Option<&str>,
63+
) -> CliResult<()> {
64+
let client = conn_mgr.create_enterprise_client(profile_name).await?;
65+
let handler = DiagnosticsHandler::new(client.clone());
66+
67+
match self {
68+
DiagnosticsCommands::Get => {
69+
let config = handler
70+
.get_config()
71+
.await
72+
.context("Failed to get diagnostics configuration")?;
73+
74+
let output_data = if let Some(q) = query {
75+
super::utils::apply_jmespath(&config, q)?
76+
} else {
77+
config
78+
};
79+
super::utils::print_formatted_output(output_data, output_format)?;
80+
}
81+
82+
DiagnosticsCommands::Update { data } => {
83+
let json_data = super::utils::read_json_data(data)?;
84+
let result = handler
85+
.update_config(json_data)
86+
.await
87+
.context("Failed to update diagnostics configuration")?;
88+
89+
let output_data = if let Some(q) = query {
90+
super::utils::apply_jmespath(&result, q)?
91+
} else {
92+
result
93+
};
94+
super::utils::print_formatted_output(output_data, output_format)?;
95+
}
96+
97+
DiagnosticsCommands::Run {
98+
checks,
99+
nodes,
100+
databases,
101+
} => {
102+
// Create the request directly as JSON
103+
let mut request = serde_json::json!({});
104+
105+
if let Some(checks_list) = parse_comma_separated(checks) {
106+
request["checks"] = serde_json::json!(checks_list);
107+
}
108+
109+
if let Some(nodes_list) = parse_comma_separated_u32(nodes) {
110+
request["node_uids"] = serde_json::json!(nodes_list);
111+
}
112+
113+
if let Some(databases_list) = parse_comma_separated_u32(databases) {
114+
request["bdb_uids"] = serde_json::json!(databases_list);
115+
}
116+
117+
// Use the raw POST method
118+
let report: serde_json::Value = client
119+
.post("/v1/diagnostics", &request)
120+
.await
121+
.context("Failed to run diagnostic checks")?;
122+
123+
let output_data = if let Some(q) = query {
124+
super::utils::apply_jmespath(&report, q)?
125+
} else {
126+
report
127+
};
128+
super::utils::print_formatted_output(output_data, output_format)?;
129+
}
130+
131+
DiagnosticsCommands::ListChecks => {
132+
let checks = handler
133+
.list_checks()
134+
.await
135+
.context("Failed to list available diagnostic checks")?;
136+
137+
// Convert to JSON Value for output
138+
let response = serde_json::to_value(&checks)?;
139+
140+
let output_data = if let Some(q) = query {
141+
super::utils::apply_jmespath(&response, q)?
142+
} else {
143+
response
144+
};
145+
super::utils::print_formatted_output(output_data, output_format)?;
146+
}
147+
148+
DiagnosticsCommands::LastReport => {
149+
let report = handler
150+
.get_last_report()
151+
.await
152+
.context("Failed to get last diagnostic report")?;
153+
154+
// Convert to JSON Value for output
155+
let response = serde_json::to_value(&report)?;
156+
157+
let output_data = if let Some(q) = query {
158+
super::utils::apply_jmespath(&response, q)?
159+
} else {
160+
response
161+
};
162+
super::utils::print_formatted_output(output_data, output_format)?;
163+
}
164+
165+
DiagnosticsCommands::GetReport { report_id } => {
166+
let report = handler
167+
.get_report(report_id)
168+
.await
169+
.context(format!("Failed to get diagnostic report {}", report_id))?;
170+
171+
// Convert to JSON Value for output
172+
let response = serde_json::to_value(&report)?;
173+
174+
let output_data = if let Some(q) = query {
175+
super::utils::apply_jmespath(&response, q)?
176+
} else {
177+
response
178+
};
179+
super::utils::print_formatted_output(output_data, output_format)?;
180+
}
181+
182+
DiagnosticsCommands::ListReports => {
183+
let reports = handler
184+
.list_reports()
185+
.await
186+
.context("Failed to list diagnostic reports")?;
187+
188+
// Convert to JSON Value for output
189+
let response = serde_json::to_value(&reports)?;
190+
191+
let output_data = if let Some(q) = query {
192+
super::utils::apply_jmespath(&response, q)?
193+
} else {
194+
response
195+
};
196+
super::utils::print_formatted_output(output_data, output_format)?;
197+
}
198+
}
199+
200+
Ok(())
201+
}
202+
}
203+
204+
pub async fn handle_diagnostics_command(
205+
conn_mgr: &ConnectionManager,
206+
profile_name: Option<&str>,
207+
diagnostics_cmd: DiagnosticsCommands,
208+
output_format: OutputFormat,
209+
query: Option<&str>,
210+
) -> CliResult<()> {
211+
diagnostics_cmd
212+
.execute(conn_mgr, profile_name, output_format, query)
213+
.await
214+
}
215+
216+
// Helper functions
217+
fn parse_comma_separated(input: &Option<String>) -> Option<Vec<String>> {
218+
input.as_ref().map(|s| {
219+
s.split(',')
220+
.map(|item| item.trim().to_string())
221+
.filter(|item| !item.is_empty())
222+
.collect()
223+
})
224+
}
225+
226+
fn parse_comma_separated_u32(input: &Option<String>) -> Option<Vec<u32>> {
227+
input.as_ref().and_then(|s| {
228+
let values: Result<Vec<u32>, _> = s
229+
.split(',')
230+
.map(|item| item.trim())
231+
.filter(|item| !item.is_empty())
232+
.map(|item| item.parse::<u32>())
233+
.collect();
234+
values.ok()
235+
})
236+
}

crates/redisctl/src/commands/enterprise/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod crdb;
66
pub mod crdb_impl;
77
pub mod database;
88
pub mod database_impl;
9+
pub mod diagnostics;
910
pub mod logs;
1011
pub mod logs_impl;
1112
pub mod module;

crates/redisctl/src/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ async fn execute_enterprise_command(
202202
)
203203
.await
204204
}
205+
Diagnostics(diagnostics_cmd) => {
206+
commands::enterprise::diagnostics::handle_diagnostics_command(
207+
conn_mgr,
208+
profile,
209+
diagnostics_cmd.clone(),
210+
output,
211+
query,
212+
)
213+
.await
214+
}
205215
Node(node_cmd) => {
206216
commands::enterprise::node::handle_node_command(
207217
conn_mgr, profile, node_cmd, output, query,

0 commit comments

Comments
 (0)