use serde_json::{Map, Value as JsonValue};
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::server::response_shape::types::{DdlColType, ShapedRows};
use crate::control::state::SharedState;
use super::super::super::result::{DdlError, DdlResult};
fn err(sqlstate: &str, message: String) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message,
}
}
pub fn show_alerts(
state: &SharedState,
identity: &AuthenticatedIdentity,
database_id: nodedb_types::DatabaseId,
) -> Result<Vec<DdlResult>, DdlError> {
let tenant_id = identity.tenant_id.as_u64();
let alerts = state
.alert_registry
.list_for_tenant_in_database(database_id.as_u64(), tenant_id);
let columns = vec![
"name".to_string(),
"collection".to_string(),
"condition".to_string(),
"group_by".to_string(),
"window".to_string(),
"fire_after".to_string(),
"recover_after".to_string(),
"severity".to_string(),
"enabled".to_string(),
"notify_count".to_string(),
];
let column_types = vec![
DdlColType::Text,
DdlColType::Text,
DdlColType::Text,
DdlColType::Text,
DdlColType::Text,
DdlColType::Int8,
DdlColType::Int8,
DdlColType::Text,
DdlColType::Text,
DdlColType::Int8,
];
let mut rows = Vec::new();
for alert in &alerts {
let condition_str = format!(
"{}({}) {} {}",
alert.condition.agg_func,
alert.condition.column,
alert.condition.op.as_sql(),
alert.condition.threshold,
);
let group_by_str = if alert.group_by.is_empty() {
"-".to_string()
} else {
alert.group_by.join(", ")
};
let window_str = format_duration_ms(alert.window_ms);
let mut row = Map::new();
row.insert("name".to_string(), JsonValue::String(alert.name.clone()));
row.insert(
"collection".to_string(),
JsonValue::String(alert.collection.clone()),
);
row.insert("condition".to_string(), JsonValue::String(condition_str));
row.insert("group_by".to_string(), JsonValue::String(group_by_str));
row.insert("window".to_string(), JsonValue::String(window_str));
row.insert(
"fire_after".to_string(),
JsonValue::String((alert.fire_after as i64).to_string()),
);
row.insert(
"recover_after".to_string(),
JsonValue::String((alert.recover_after as i64).to_string()),
);
row.insert(
"severity".to_string(),
JsonValue::String(alert.severity.clone()),
);
row.insert(
"enabled".to_string(),
JsonValue::String(alert.enabled.to_string()),
);
row.insert(
"notify_count".to_string(),
JsonValue::String((alert.notify_targets.len() as i64).to_string()),
);
rows.push(row);
}
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows,
notice: None,
})])
}
pub fn show_alert_status(
state: &SharedState,
identity: &AuthenticatedIdentity,
database_id: nodedb_types::DatabaseId,
name: &str,
) -> Result<Vec<DdlResult>, DdlError> {
let tenant_id = identity.tenant_id.as_u64();
let name = name.to_lowercase();
if state
.alert_registry
.get(database_id.as_u64(), tenant_id, &name)
.is_none()
{
return Err(err("42704", format!("alert '{name}' does not exist")));
}
let states = state.alert_hysteresis.list_states(tenant_id, &name);
let columns = vec![
"group_key".to_string(),
"status".to_string(),
"consecutive_fire".to_string(),
"consecutive_recover".to_string(),
"last_value".to_string(),
"fired_at".to_string(),
"cleared_at".to_string(),
];
let column_types = vec![
DdlColType::Text,
DdlColType::Text,
DdlColType::Int8,
DdlColType::Int8,
DdlColType::Text,
DdlColType::Int8,
DdlColType::Int8,
];
let mut rows = Vec::new();
for (group_key, group_state) in &states {
let status_str = match group_state.status {
crate::event::alert::AlertStatus::Active => "ACTIVE",
crate::event::alert::AlertStatus::Cleared => "CLEARED",
};
let last_value = group_state
.last_value
.map(|v| format!("{v:.4}"))
.unwrap_or_else(|| "-".to_string());
let mut row = Map::new();
row.insert(
"group_key".to_string(),
JsonValue::String(group_key.clone()),
);
row.insert(
"status".to_string(),
JsonValue::String(status_str.to_string()),
);
row.insert(
"consecutive_fire".to_string(),
JsonValue::String((group_state.consecutive_fire as i64).to_string()),
);
row.insert(
"consecutive_recover".to_string(),
JsonValue::String((group_state.consecutive_recover as i64).to_string()),
);
row.insert("last_value".to_string(), JsonValue::String(last_value));
row.insert(
"fired_at".to_string(),
JsonValue::String(
group_state
.fired_at
.map(|t| t as i64)
.unwrap_or(0)
.to_string(),
),
);
row.insert(
"cleared_at".to_string(),
JsonValue::String(
group_state
.cleared_at
.map(|t| t as i64)
.unwrap_or(0)
.to_string(),
),
);
rows.push(row);
}
Ok(vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows,
notice: None,
})])
}
fn format_duration_ms(ms: u64) -> String {
const MINUTE: u64 = 60_000;
const HOUR: u64 = 3_600_000;
const DAY: u64 = 86_400_000;
if ms.is_multiple_of(DAY) {
format!("{}d", ms / DAY)
} else if ms.is_multiple_of(HOUR) {
format!("{}h", ms / HOUR)
} else if ms.is_multiple_of(MINUTE) {
format!("{}m", ms / MINUTE)
} else if ms.is_multiple_of(1_000) {
format!("{}s", ms / 1_000)
} else {
format!("{ms}ms")
}
}