use std::io::{self, BufRead, IsTerminal, Write};
use anyhow::{bail, Context, Result};
use serde_json::{json, Value};
use crate::api::client::ApiClient;
use crate::api::error::ApiError;
use crate::config::credentials::resolve_api_key;
use crate::config::manager::ConfigManager;
use crate::model::loader::Models;
use super::dispatch::{print_output_with_opts, OutputOpts};
use super::formatter;
const ALERT_METRICS: &[&str] = &[
"CU_COMPUTATION",
"CU_CAPACITY",
"REQ_SEARCH_COUNT",
"REQ_QUERY_COUNT",
"REQ_SEARCH_LATENCY_P99",
"REQ_QUERY_LATENCY_P99",
"REQ_SEARCH_FAILURE_RATE",
"REQ_QUERY_FAILURE_RATE",
"TOTAL_ENTITIES",
"CREDIT_CARD_EXPIRATION",
"FREE_CREDITS_BALANCE",
"WALLET_BALANCE",
"DAILY_USAGE",
];
const COMPARISON_MAP: &[(&str, &str)] = &[
(">", "GREATER_THAN"),
("gt", "GREATER_THAN"),
("greater_than", "GREATER_THAN"),
("<", "LESS_THAN"),
("lt", "LESS_THAN"),
("less_than", "LESS_THAN"),
(">=", "GREATER_THAN_OR_EQUAL"),
("gte", "GREATER_THAN_OR_EQUAL"),
("greater_than_or_equal", "GREATER_THAN_OR_EQUAL"),
("<=", "LESS_THAN_OR_EQUAL"),
("lte", "LESS_THAN_OR_EQUAL"),
("less_than_or_equal", "LESS_THAN_OR_EQUAL"),
("=", "EQUAL"),
("eq", "EQUAL"),
("equal", "EQUAL"),
];
const COMPARISON_SYMBOLS: &[(&str, &str)] = &[
("GREATER_THAN", ">"),
("LESS_THAN", "<"),
("GREATER_THAN_OR_EQUAL", ">="),
("LESS_THAN_OR_EQUAL", "<="),
("EQUAL", "="),
];
const ACTION_CONFIG_KEYS: &[(&str, &str)] = &[
("email", "email"),
("slack", "webhookUrl"),
("webhook", "url"),
("pagerduty", "routingKey"),
("opsgenie", "apiKey"),
("wecom", "webhookUrl"),
("dingtalk", "webhookUrl"),
("lark", "webhookUrl"),
];
pub fn resolve_comparison(input: &str) -> Result<String> {
let key = input.to_lowercase();
let key = key.trim();
for (k, v) in COMPARISON_MAP {
if *k == key {
return Ok(v.to_string());
}
}
bail!(
"Invalid comparison '{}'. Use: >, <, >=, <=, = (or gt, lt, gte, lte, eq)",
input
);
}
pub fn comparison_symbol(api_value: &str) -> &str {
COMPARISON_SYMBOLS
.iter()
.find(|(k, _)| *k == api_value)
.map(|(_, v)| *v)
.unwrap_or("?")
}
pub fn parse_action(action_str: &str) -> Result<Value> {
let parts: Vec<&str> = action_str.splitn(2, ':').collect();
if parts.len() != 2 {
bail!(
"Invalid action format '{}'. Use type:config (e.g. email:user@example.com)",
action_str
);
}
let action_type = parts[0].to_lowercase();
let config_value = parts[1].trim();
let config_key = ACTION_CONFIG_KEYS
.iter()
.find(|(k, _)| *k == action_type)
.map(|(_, v)| *v);
match config_key {
Some(key) => Ok(json!({
"type": action_type.to_uppercase(),
"config": { key: config_value }
})),
None => {
let valid: Vec<&str> = ACTION_CONFIG_KEYS.iter().map(|(k, _)| *k).collect();
bail!(
"Unknown action type '{}'. Valid types: {}",
action_type,
valid.join(", ")
);
}
}
}
async fn prompt_project_for_alert(client: &ApiClient) -> Result<String> {
let result = client.call("GET", "/v2/projects", None, None).await?;
let projects = result
.get("projects")
.or_else(|| result.get("data").and_then(|d| d.get("projects")))
.and_then(|v| v.as_array())
.context("Failed to fetch projects")?;
if projects.is_empty() {
bail!("No projects found.");
}
if projects.len() == 1 {
let name = projects[0]
.get("projectName")
.and_then(|v| v.as_str())
.unwrap_or("?");
let id = projects[0]
.get("projectId")
.and_then(|v| v.as_str())
.context("Project has no ID")?;
eprintln!("Using project: {} ({})", name, id);
return Ok(id.to_string());
}
let stdin = io::stdin();
let stderr = io::stderr();
eprintln!("Select project:");
for (i, p) in projects.iter().enumerate() {
let name = p.get("projectName").and_then(|v| v.as_str()).unwrap_or("?");
let id = p.get("projectId").and_then(|v| v.as_str()).unwrap_or("?");
eprintln!(" {}) {} ({})", i + 1, name, id);
}
loop {
eprint!("Select [1-{}]: ", projects.len());
stderr.lock().flush()?;
let mut input = String::new();
let n_read = stdin.lock().read_line(&mut input)?;
if n_read == 0 {
bail!("EOF on stdin");
}
if let Ok(n) = input.trim().parse::<usize>() {
if n >= 1 && n <= projects.len() {
let id = projects[n - 1]
.get("projectId")
.and_then(|v| v.as_str())
.context("Project has no ID")?;
return Ok(id.to_string());
}
}
eprintln!("Invalid selection, please try again.");
}
}
fn prompt_metric_select() -> Result<String> {
let stdin = io::stdin();
let stderr = io::stderr();
eprintln!("Select metric:");
for (i, name) in ALERT_METRICS.iter().enumerate() {
eprintln!(" {}) {}", i + 1, name);
}
loop {
eprint!("Select [1-{}]: ", ALERT_METRICS.len());
stderr.lock().flush()?;
let mut input = String::new();
let n_read = stdin.lock().read_line(&mut input)?;
if n_read == 0 {
bail!("EOF on stdin");
}
if let Ok(n) = input.trim().parse::<usize>() {
if n >= 1 && n <= ALERT_METRICS.len() {
return Ok(ALERT_METRICS[n - 1].to_string());
}
}
eprintln!("Invalid selection, please try again.");
}
}
fn prompt_comparison_select() -> Result<String> {
let choices = &[
(">", "greater than"),
("<", "less than"),
(">=", "greater than or equal"),
("<=", "less than or equal"),
("=", "equal"),
];
let stdin = io::stdin();
let stderr = io::stderr();
eprintln!("Select comparison operator:");
for (i, (sym, desc)) in choices.iter().enumerate() {
eprintln!(" {}) {} ({})", i + 1, sym, desc);
}
loop {
eprint!("Select [1-{}]: ", choices.len());
stderr.lock().flush()?;
let mut input = String::new();
let n_read = stdin.lock().read_line(&mut input)?;
if n_read == 0 {
bail!("EOF on stdin");
}
if let Ok(n) = input.trim().parse::<usize>() {
if n >= 1 && n <= choices.len() {
return Ok(choices[n - 1].0.to_string());
}
}
eprintln!("Invalid selection, please try again.");
}
}
fn prompt_threshold() -> Result<String> {
let stdin = io::stdin();
let stderr = io::stderr();
loop {
eprint!("Threshold value: ");
stderr.lock().flush()?;
let mut input = String::new();
let n_read = stdin.lock().read_line(&mut input)?;
if n_read == 0 {
bail!("EOF on stdin");
}
let val = input.trim().to_string();
if !val.is_empty() {
return Ok(val);
}
eprintln!("Invalid input, please try again.");
}
}
async fn prompt_alert_id(client: &ApiClient, project_id: Option<String>) -> Result<String> {
let project_id = match project_id {
Some(id) => id,
None => prompt_project_for_alert(client).await?,
};
let body = json!({
"projectId": project_id,
"pageSize": 100,
"currentPage": 1,
});
let alerts = match client
.call("GET", "/v2/alertRules", None, Some(&body))
.await
{
Ok(result) => result
.get("alertRules")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default(),
Err(e) => {
eprintln!("Warning: failed to fetch alerts: {}", e);
vec![]
}
};
if alerts.is_empty() {
let stdin = io::stdin();
let stderr = io::stderr();
loop {
eprint!("Alert rule ID: ");
stderr.lock().flush()?;
let mut input = String::new();
let n_read = stdin.lock().read_line(&mut input)?;
if n_read == 0 {
bail!("EOF on stdin");
}
let val = input.trim().to_string();
if !val.is_empty() {
return Ok(val);
}
eprintln!("Alert rule ID cannot be empty, please try again.");
}
}
if alerts.len() == 1 {
let a = &alerts[0];
let aid = a.get("id").and_then(|v| v.as_str()).unwrap_or("?");
let name = a
.get("ruleName")
.and_then(|v| v.as_str())
.or_else(|| a.get("metricName").and_then(|v| v.as_str()))
.unwrap_or(aid);
let status = if a.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false) {
"on"
} else {
"off"
};
eprintln!("alert: {} [{}] ({}) (auto-selected)", name, status, aid);
return Ok(aid.to_string());
}
let stdin = io::stdin();
let stderr = io::stderr();
eprintln!("Select alert rule:");
for (i, a) in alerts.iter().enumerate() {
let aid = a.get("id").and_then(|v| v.as_str()).unwrap_or("?");
let name = a
.get("ruleName")
.and_then(|v| v.as_str())
.or_else(|| a.get("metricName").and_then(|v| v.as_str()))
.unwrap_or(aid);
let status = if a.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false) {
"on"
} else {
"off"
};
eprintln!(" {}) {} [{}] ({})", i + 1, name, status, aid);
}
loop {
eprint!("Select [1-{}]: ", alerts.len());
stderr.lock().flush()?;
let mut input = String::new();
let n_read = stdin.lock().read_line(&mut input)?;
if n_read == 0 {
bail!("EOF on stdin");
}
if let Ok(n) = input.trim().parse::<usize>() {
if n >= 1 && n <= alerts.len() {
let aid = alerts[n - 1]
.get("id")
.and_then(|v| v.as_str())
.context("Alert has no ID")?;
return Ok(aid.to_string());
}
}
eprintln!("Invalid selection, please try again.");
}
}
fn make_client(
models: &Models,
api_key_override: Option<&str>,
config_mgr: &ConfigManager,
) -> Result<ApiClient> {
let api_key =
resolve_api_key(api_key_override, config_mgr).ok_or_else(|| ApiError::NoApiKey)?;
let base_url =
super::endpoint::resolve_control_plane_url(config_mgr, &models.control_plane, None);
Ok(ApiClient::new(api_key, base_url))
}
fn is_help(args: &[String]) -> bool {
args.iter().any(|a| a == "-h" || a == "--help")
}
fn next_val<'a>(args: &'a [String], i: &mut usize) -> Option<&'a str> {
let next_i = *i + 1;
let val = args
.get(next_i)
.map(|s| s.as_str())
.filter(|s| !s.starts_with("--"));
if val.is_some() {
*i = next_i;
}
val
}
pub async fn run_from_args(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
let subcmd = raw_args.first().map(|s| s.as_str()).unwrap_or("");
let rest: Vec<String> = if raw_args.len() > 1 {
raw_args[1..].to_vec()
} else {
vec![]
};
match subcmd {
"list" => list(models, config_mgr, &rest, output_opts).await,
"create" => create(models, config_mgr, &rest, output_opts).await,
"update" => update(models, config_mgr, &rest, output_opts).await,
"delete" => delete(models, config_mgr, &rest, output_opts).await,
"enable" => enable(models, config_mgr, &rest, output_opts).await,
"disable" => disable(models, config_mgr, &rest, output_opts).await,
_ => bail!(
"Unknown alert operation '{}'. Available: list, create, update, delete, enable, disable",
subcmd
),
}
}
async fn list(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print!(
"List alert rules.\n\n\
Usage: zilliz alert list [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Project ID (required)\n\
\x20 {:24}{:30}Items per page\n\
\x20 {:24}{:30}Page number\n",
"--project-id", "<string>", "--page-size", "<integer>", "--page", "<integer>",
);
return Ok(());
}
let mut project_id = None;
let mut page_size: Option<i64> = None;
let mut page: Option<i64> = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--project-id" => project_id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--page-size" => page_size = next_val(raw_args, &mut i).and_then(|s| s.parse().ok()),
"--page" => page = next_val(raw_args, &mut i).and_then(|s| s.parse().ok()),
_ => {}
}
i += 1;
}
let client = make_client(models, output_opts.api_key, config_mgr)?;
let project_id = match project_id {
Some(id) => id,
None if io::stdin().is_terminal() => prompt_project_for_alert(&client).await?,
None => bail!("Missing required option: --project-id"),
};
let mut body = json!({ "projectId": project_id });
if let Some(ps) = page_size {
body["pageSize"] = json!(ps);
}
if let Some(p) = page {
body["currentPage"] = json!(p);
}
let result = client
.call("GET", "/v2/alertRules", None, Some(&body))
.await?;
if output_opts.format == "table" && output_opts.query.is_none() {
print_alert_table(&result, output_opts.no_header);
} else {
print_output_with_opts(&result, output_opts, None);
}
Ok(())
}
async fn create(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print!(
"Create a new alert rule.\n\n\
Usage: zilliz alert create [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Project ID (required)\n\
\x20 {:24}{:30}Metric name, e.g. CU_COMPUTATION (required)\n\
\x20 {:24}{:30}Threshold value (required)\n\
\x20 {:24}{:30}Operator: >, <, >=, <=, = (or gt, lt, gte, lte, eq) (required)\n\
\x20 {:24}{:30}Rule name (defaults to \"<METRIC> alert\")\n\
\x20 {:24}{:30}Alert level: WARNING or CRITICAL [default: WARNING]\n\
\x20 {:24}{:30}Time window (e.g. 5m, 1h)\n\
\x20 {:24}{:30}Target cluster ID (repeatable)\n\
\x20 {:24}{:30}Action type:config (e.g. email:user@example.com, repeatable)\n\
\x20 {:24}{:30}Send notification when resolved\n\
\x20 {:24}{:30}Repeat interval in seconds\n\
\x20 {:24}{:30}Whether the rule is enabled\n",
"--project-id",
"<string>",
"--metric-name",
"<string>",
"--threshold",
"<string>",
"--comparison",
"<string>",
"--rule-name",
"<string>",
"--level",
"<string>",
"--window-size",
"<string>",
"--cluster-id",
"<string>",
"--action",
"<string>",
"--send-resolved",
"<bool>",
"--repeat-interval",
"<integer>",
"--enabled",
"<bool>",
);
return Ok(());
}
let mut project_id = None;
let mut metric_name = None;
let mut threshold = None;
let mut comparison = None;
let mut rule_name = None;
let mut level = "WARNING".to_string();
let mut window_size = None;
let mut cluster_ids: Vec<String> = Vec::new();
let mut actions: Vec<String> = Vec::new();
let mut send_resolved: Option<bool> = None;
let mut repeat_interval: Option<i64> = None;
let mut enabled: Option<bool> = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--project-id" => project_id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--metric-name" => metric_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--threshold" => threshold = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--comparison" => comparison = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--rule-name" => rule_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--level" => {
if let Some(v) = next_val(raw_args, &mut i) {
level = v.to_string();
}
}
"--window-size" => window_size = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--cluster-id" => {
if let Some(v) = next_val(raw_args, &mut i) {
cluster_ids.push(v.to_string());
}
}
"--action" => {
if let Some(v) = next_val(raw_args, &mut i) {
actions.push(v.to_string());
}
}
"--send-resolved" => {
send_resolved = next_val(raw_args, &mut i).and_then(|s| s.parse().ok())
}
"--repeat-interval" => {
repeat_interval = next_val(raw_args, &mut i).and_then(|s| s.parse().ok())
}
"--enabled" => enabled = next_val(raw_args, &mut i).and_then(|s| s.parse().ok()),
_ => {}
}
i += 1;
}
let client = make_client(models, output_opts.api_key, config_mgr)?;
let is_tty = io::stdin().is_terminal();
let project_id = match project_id {
Some(id) => id,
None if is_tty => prompt_project_for_alert(&client).await?,
None => bail!("Missing required option: --project-id"),
};
let metric_name = match metric_name {
Some(name) => name,
None if is_tty => prompt_metric_select()?,
None => bail!("Missing required option: --metric-name"),
};
let comparison = match comparison {
Some(c) => c,
None if is_tty => prompt_comparison_select()?,
None => bail!("Missing required option: --comparison"),
};
let threshold = match threshold {
Some(t) => t,
None if is_tty => prompt_threshold()?,
None => bail!("Missing required option: --threshold"),
};
let comparison_method = resolve_comparison(&comparison)?;
let parsed_actions: Vec<Value> = actions
.iter()
.map(|a| parse_action(a))
.collect::<Result<Vec<_>>>()?;
let mut body = json!({
"projectId": project_id,
"metricName": metric_name.to_uppercase(),
"threshold": threshold,
"comparisonMethod": comparison_method,
"level": level.to_uppercase(),
});
let obj = body.as_object_mut().unwrap();
if let Some(name) = rule_name {
obj.insert("ruleName".to_string(), json!(name));
} else {
obj.insert(
"ruleName".to_string(),
json!(format!("{} alert", metric_name.to_uppercase())),
);
}
if let Some(ws) = window_size {
obj.insert("windowSize".to_string(), json!(ws));
}
if !cluster_ids.is_empty() {
obj.insert("targetInstanceIds".to_string(), json!(cluster_ids));
}
if !parsed_actions.is_empty() {
obj.insert("actions".to_string(), json!(parsed_actions));
}
if let Some(sr) = send_resolved {
obj.insert("sendResolved".to_string(), json!(sr));
}
if let Some(ri) = repeat_interval {
obj.insert("repeatIntervalSeconds".to_string(), json!(ri));
}
if let Some(en) = enabled {
obj.insert("enabled".to_string(), json!(en));
}
let result = client
.call("POST", "/v2/alertRules", None, Some(&body))
.await?;
if output_opts.format == "table" {
println!("Alert rule created successfully.");
if let Some(id) = result.get("id").and_then(|v| v.as_str()) {
println!(" ID: {}", id);
}
} else {
print_output_with_opts(&result, output_opts, None);
}
Ok(())
}
async fn update(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print!(
"Update an existing alert rule.\n\n\
Usage: zilliz alert update [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Alert rule ID (required)\n\
\x20 {:24}{:30}Project ID (for interactive --id selection)\n\
\x20 {:24}{:30}Rule name\n\
\x20 {:24}{:30}Metric name\n\
\x20 {:24}{:30}Threshold value\n\
\x20 {:24}{:30}Comparison operator\n\
\x20 {:24}{:30}Alert level: WARNING or CRITICAL\n\
\x20 {:24}{:30}Time window\n\
\x20 {:24}{:30}Target cluster ID (repeatable, replaces existing)\n\
\x20 {:24}{:30}Action type:config (repeatable, replaces existing)\n\
\x20 {:24}{:30}Send notification when resolved\n\
\x20 {:24}{:30}Repeat interval in seconds\n\
\x20 {:24}{:30}Whether the rule is enabled\n",
"--id",
"<string>",
"--project-id",
"<string>",
"--rule-name",
"<string>",
"--metric-name",
"<string>",
"--threshold",
"<string>",
"--comparison",
"<string>",
"--level",
"<string>",
"--window-size",
"<string>",
"--cluster-id",
"<string>",
"--action",
"<string>",
"--send-resolved",
"<bool>",
"--repeat-interval",
"<integer>",
"--enabled",
"<bool>",
);
return Ok(());
}
let mut id = None;
let mut project_id = None;
let mut rule_name = None;
let mut metric_name = None;
let mut threshold = None;
let mut comparison = None;
let mut level_opt = None;
let mut window_size = None;
let mut cluster_ids: Vec<String> = Vec::new();
let mut actions: Vec<String> = Vec::new();
let mut send_resolved: Option<bool> = None;
let mut repeat_interval: Option<i64> = None;
let mut enabled: Option<bool> = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--id" => id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--project-id" => project_id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--rule-name" => rule_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--metric-name" => metric_name = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--threshold" => threshold = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--comparison" => comparison = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--level" => level_opt = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--window-size" => window_size = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--cluster-id" => {
if let Some(v) = next_val(raw_args, &mut i) {
cluster_ids.push(v.to_string());
}
}
"--action" => {
if let Some(v) = next_val(raw_args, &mut i) {
actions.push(v.to_string());
}
}
"--send-resolved" => {
send_resolved = next_val(raw_args, &mut i).and_then(|s| s.parse().ok())
}
"--repeat-interval" => {
repeat_interval = next_val(raw_args, &mut i).and_then(|s| s.parse().ok())
}
"--enabled" => enabled = next_val(raw_args, &mut i).and_then(|s| s.parse().ok()),
_ => {}
}
i += 1;
}
let client = make_client(models, output_opts.api_key, config_mgr)?;
let id = match id {
Some(id) => id,
None if io::stdin().is_terminal() => prompt_alert_id(&client, project_id).await?,
None => bail!("Missing required option: --id"),
};
let mut body = serde_json::Map::new();
if let Some(name) = rule_name {
body.insert("ruleName".to_string(), json!(name));
}
if let Some(m) = metric_name {
body.insert("metricName".to_string(), json!(m.to_uppercase()));
}
if let Some(t) = threshold {
body.insert("threshold".to_string(), json!(t));
}
if let Some(c) = comparison {
body.insert(
"comparisonMethod".to_string(),
json!(resolve_comparison(&c)?),
);
}
if let Some(l) = level_opt {
body.insert("level".to_string(), json!(l.to_uppercase()));
}
if let Some(ws) = window_size {
body.insert("windowSize".to_string(), json!(ws));
}
if !cluster_ids.is_empty() {
body.insert("targetInstanceIds".to_string(), json!(cluster_ids));
}
if !actions.is_empty() {
let parsed_actions: Vec<Value> = actions
.iter()
.map(|a| parse_action(a))
.collect::<Result<Vec<_>>>()?;
body.insert("actions".to_string(), json!(parsed_actions));
}
if let Some(sr) = send_resolved {
body.insert("sendResolved".to_string(), json!(sr));
}
if let Some(ri) = repeat_interval {
body.insert("repeatIntervalSeconds".to_string(), json!(ri));
}
if let Some(en) = enabled {
body.insert("enabled".to_string(), json!(en));
}
if body.is_empty() {
bail!("No fields to update.");
}
let path = format!("/v2/alertRules/{}", urlencoding::encode(&id));
let body_val = Value::Object(body);
let result = client.call("PUT", &path, None, Some(&body_val)).await?;
if output_opts.format == "table" {
println!("Alert rule {} updated successfully.", id);
} else {
print_output_with_opts(&result, output_opts, None);
}
Ok(())
}
async fn delete(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print!(
"Delete an alert rule.\n\n\
Usage: zilliz alert delete [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Alert rule ID (required)\n\
\x20 {:24}{:30}Project ID (for interactive --id selection)\n\
\x20 {:24}{:30}Skip confirmation prompt\n",
"--id", "<string>", "--project-id", "<string>", "-y, --yes", "",
);
return Ok(());
}
let mut id = None;
let mut project_id = None;
let mut yes = false;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--id" => id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--project-id" => project_id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"-y" | "--yes" => yes = true,
_ => {}
}
i += 1;
}
let client = make_client(models, output_opts.api_key, config_mgr)?;
let id = match id {
Some(id) => id,
None if io::stdin().is_terminal() => prompt_alert_id(&client, project_id).await?,
None => bail!("Missing required option: --id"),
};
if !yes {
print!("Delete alert rule {}? [y/N] ", id);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("Aborted.");
return Ok(());
}
}
let path = format!("/v2/alertRules/{}", urlencoding::encode(&id));
let _ = client.call("DELETE", &path, None, None).await?;
println!("Alert rule {} deleted.", id);
Ok(())
}
async fn enable(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print!(
"Enable an alert rule.\n\n\
Usage: zilliz alert enable [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Alert rule ID (required)\n\
\x20 {:24}{:30}Project ID (for interactive --id selection)\n",
"--id", "<string>", "--project-id", "<string>",
);
return Ok(());
}
let mut id = None;
let mut project_id = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--id" => id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--project-id" => project_id = next_val(raw_args, &mut i).map(|s| s.to_string()),
_ => {}
}
i += 1;
}
let client = make_client(models, output_opts.api_key, config_mgr)?;
let id = match id {
Some(id) => id,
None if io::stdin().is_terminal() => prompt_alert_id(&client, project_id).await?,
None => bail!("Missing required option: --id"),
};
let path = format!("/v2/alertRules/{}", urlencoding::encode(&id));
let body = json!({"enabled": true});
let _ = client.call("PUT", &path, None, Some(&body)).await?;
println!("Alert rule {} enabled.", id);
Ok(())
}
async fn disable(
models: &Models,
config_mgr: &ConfigManager,
raw_args: &[String],
output_opts: &OutputOpts<'_>,
) -> Result<()> {
if is_help(raw_args) {
print!(
"Disable an alert rule.\n\n\
Usage: zilliz alert disable [OPTIONS]\n\n\
Options:\n\
\x20 {:24}{:30}Alert rule ID (required)\n\
\x20 {:24}{:30}Project ID (for interactive --id selection)\n",
"--id", "<string>", "--project-id", "<string>",
);
return Ok(());
}
let mut id = None;
let mut project_id = None;
let mut i = 0;
while i < raw_args.len() {
match raw_args[i].as_str() {
"--id" => id = next_val(raw_args, &mut i).map(|s| s.to_string()),
"--project-id" => project_id = next_val(raw_args, &mut i).map(|s| s.to_string()),
_ => {}
}
i += 1;
}
let client = make_client(models, output_opts.api_key, config_mgr)?;
let id = match id {
Some(id) => id,
None if io::stdin().is_terminal() => prompt_alert_id(&client, project_id).await?,
None => bail!("Missing required option: --id"),
};
let path = format!("/v2/alertRules/{}", urlencoding::encode(&id));
let body = json!({"enabled": false});
let _ = client.call("PUT", &path, None, Some(&body)).await?;
println!("Alert rule {} disabled.", id);
Ok(())
}
fn print_alert_table(result: &Value, no_header: bool) {
let rules = result
.get("alertRules")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if rules.is_empty() {
println!("No alert rules found.");
return;
}
let columns = &[
"ID",
"Rule Name",
"Metric",
"Level",
"Enabled",
"Condition",
"Actions",
];
let rows: Vec<Vec<String>> = rules
.iter()
.map(|r| {
let id = r
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let name = r
.get("ruleName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let metric = r
.get("metricName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let level_raw = r.get("level").and_then(|v| v.as_str()).unwrap_or("");
let is_tty = super::formatter::terminal_width().is_some();
let level = if is_tty {
match level_raw {
"CRITICAL" => format!("\x1b[31m{}\x1b[0m", level_raw),
"WARNING" => format!("\x1b[33m{}\x1b[0m", level_raw),
_ => level_raw.to_string(),
}
} else {
level_raw.to_string()
};
let enabled_val = r.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
let enabled = if is_tty {
if enabled_val {
"\x1b[32mtrue\x1b[0m".to_string()
} else {
"\x1b[2mfalse\x1b[0m".to_string()
}
} else if enabled_val {
"true".to_string()
} else {
"false".to_string()
};
let cmp = r
.get("comparisonMethod")
.and_then(|v| v.as_str())
.unwrap_or("");
let threshold = r
.get("threshold")
.map(|v| match v {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default();
let condition = format!("{} {}", comparison_symbol(cmp), threshold);
let actions = r
.get("actions")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|a| a.get("type").and_then(|t| t.as_str()))
.map(|t| t.to_lowercase())
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default();
vec![id, name, metric, level, enabled, condition, actions]
})
.collect();
let mut table = formatter::create_table(columns, no_header);
for row in rows {
table.add_row(row);
}
println!("{}", table);
}