diff --git a/src/about.rs b/src/about.rs
index f2f4593b5..e2471c962 100644
--- a/src/about.rs
+++ b/src/about.rs
@@ -112,10 +112,10 @@ pub fn print_about(
current_version,
); // " " " "
- if let Some(latest_release) = latest_release {
- if latest_release.version > current_version {
- print_latest_release(latest_release);
- }
+ if let Some(latest_release) = latest_release
+ && latest_release.version > current_version
+ {
+ print_latest_release(latest_release);
}
eprintln!(
diff --git a/src/alerts/alert_enums.rs b/src/alerts/alert_enums.rs
new file mode 100644
index 000000000..10cf487e2
--- /dev/null
+++ b/src/alerts/alert_enums.rs
@@ -0,0 +1,284 @@
+/*
+ * Parseable Server (C) 2022 - 2024 Parseable, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+use std::{
+ fmt::{self, Display},
+ str::FromStr,
+};
+
+use chrono::{DateTime, Utc};
+use derive_more::derive::FromStr;
+use serde::ser::Error;
+use ulid::Ulid;
+
+use crate::alerts::{
+ alert_structs::{AnomalyConfig, ForecastConfig, RollingWindow},
+ alert_traits::AlertTrait,
+};
+
+pub enum AlertTask {
+ Create(Box),
+ Delete(Ulid),
+}
+
+#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "lowercase")]
+pub enum AlertVersion {
+ V1,
+ #[default]
+ V2,
+}
+
+impl From<&str> for AlertVersion {
+ fn from(value: &str) -> Self {
+ match value {
+ "v1" => Self::V1,
+ "v2" => Self::V2,
+ _ => Self::V2, // default to v2
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
+#[serde(rename_all = "camelCase")]
+pub enum Severity {
+ Critical,
+ High,
+ #[default]
+ Medium,
+ Low,
+}
+
+impl Display for Severity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Severity::Critical => write!(f, "Critical"),
+ Severity::High => write!(f, "High"),
+ Severity::Medium => write!(f, "Medium"),
+ Severity::Low => write!(f, "Low"),
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub enum LogicalOperator {
+ And,
+ Or,
+}
+
+impl Display for LogicalOperator {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ LogicalOperator::And => write!(f, "AND"),
+ LogicalOperator::Or => write!(f, "OR"),
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub enum AlertType {
+ Threshold,
+ Anomaly(AnomalyConfig),
+ Forecast(ForecastConfig),
+}
+
+impl Display for AlertType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ AlertType::Threshold => write!(f, "threshold"),
+ AlertType::Anomaly(_) => write!(f, "anomaly"),
+ AlertType::Forecast(_) => write!(f, "forecast"),
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub enum AlertOperator {
+ #[serde(rename = ">")]
+ GreaterThan,
+ #[serde(rename = "<")]
+ LessThan,
+ #[serde(rename = "=")]
+ Equal,
+ #[serde(rename = "!=")]
+ NotEqual,
+ #[serde(rename = ">=")]
+ GreaterThanOrEqual,
+ #[serde(rename = "<=")]
+ LessThanOrEqual,
+}
+
+impl Display for AlertOperator {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ AlertOperator::GreaterThan => write!(f, ">"),
+ AlertOperator::LessThan => write!(f, "<"),
+ AlertOperator::Equal => write!(f, "="),
+ AlertOperator::NotEqual => write!(f, "!="),
+ AlertOperator::GreaterThanOrEqual => write!(f, ">="),
+ AlertOperator::LessThanOrEqual => write!(f, "<="),
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, FromStr, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub enum WhereConfigOperator {
+ #[serde(rename = "=")]
+ Equal,
+ #[serde(rename = "!=")]
+ NotEqual,
+ #[serde(rename = "<")]
+ LessThan,
+ #[serde(rename = ">")]
+ GreaterThan,
+ #[serde(rename = "<=")]
+ LessThanOrEqual,
+ #[serde(rename = ">=")]
+ GreaterThanOrEqual,
+ #[serde(rename = "is null")]
+ IsNull,
+ #[serde(rename = "is not null")]
+ IsNotNull,
+ #[serde(rename = "ilike")]
+ ILike,
+ #[serde(rename = "contains")]
+ Contains,
+ #[serde(rename = "begins with")]
+ BeginsWith,
+ #[serde(rename = "ends with")]
+ EndsWith,
+ #[serde(rename = "does not contain")]
+ DoesNotContain,
+ #[serde(rename = "does not begin with")]
+ DoesNotBeginWith,
+ #[serde(rename = "does not end with")]
+ DoesNotEndWith,
+}
+
+impl WhereConfigOperator {
+ /// Convert the enum value to its string representation
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::Equal => "=",
+ Self::NotEqual => "!=",
+ Self::LessThan => "<",
+ Self::GreaterThan => ">",
+ Self::LessThanOrEqual => "<=",
+ Self::GreaterThanOrEqual => ">=",
+ Self::IsNull => "is null",
+ Self::IsNotNull => "is not null",
+ Self::ILike => "ilike",
+ Self::Contains => "contains",
+ Self::BeginsWith => "begins with",
+ Self::EndsWith => "ends with",
+ Self::DoesNotContain => "does not contain",
+ Self::DoesNotBeginWith => "does not begin with",
+ Self::DoesNotEndWith => "does not end with",
+ }
+ }
+}
+
+impl Display for WhereConfigOperator {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ // We can reuse our as_str method to get the string representation
+ write!(f, "{}", self.as_str())
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub enum AggregateFunction {
+ Avg,
+ Count,
+ CountDistinct,
+ Min,
+ Max,
+ Sum,
+}
+
+impl Display for AggregateFunction {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ AggregateFunction::Avg => write!(f, "Avg"),
+ AggregateFunction::Count => write!(f, "Count"),
+ AggregateFunction::CountDistinct => write!(f, "CountDistinct"),
+ AggregateFunction::Min => write!(f, "Min"),
+ AggregateFunction::Max => write!(f, "Max"),
+ AggregateFunction::Sum => write!(f, "Sum"),
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub enum EvalConfig {
+ RollingWindow(RollingWindow),
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Default, FromStr)]
+#[serde(rename_all = "camelCase")]
+pub enum AlertState {
+ Triggered,
+ #[default]
+ NotTriggered,
+ Disabled,
+}
+
+impl Display for AlertState {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ AlertState::Triggered => write!(f, "triggered"),
+ AlertState::Disabled => write!(f, "disabled"),
+ AlertState::NotTriggered => write!(f, "not-triggered"),
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Default)]
+#[serde(rename_all = "camelCase")]
+pub enum NotificationState {
+ #[default]
+ Notify,
+ /// Snoozed means the alert will evaluate but no notifications will be sent out
+ ///
+ /// It is a state which can only be set manually
+ ///
+ /// user needs to pass the timestamp or the duration (in human time) till which the alert is silenced
+ Mute(String),
+}
+
+impl Display for NotificationState {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ NotificationState::Notify => write!(f, "notify"),
+ NotificationState::Mute(till_time) => {
+ let till = match till_time.as_str() {
+ "indefinite" => DateTime::::MAX_UTC.to_rfc3339(),
+ _ => DateTime::::from_str(till_time)
+ .map_err(|e| std::fmt::Error::custom(e.to_string()))?
+ .to_rfc3339(),
+ };
+ write!(f, "{till}")
+ }
+ }
+ }
+}
diff --git a/src/alerts/alert_structs.rs b/src/alerts/alert_structs.rs
new file mode 100644
index 000000000..b734d3150
--- /dev/null
+++ b/src/alerts/alert_structs.rs
@@ -0,0 +1,383 @@
+/*
+ * Parseable Server (C) 2022 - 2024 Parseable, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+use std::collections::HashMap;
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use tokio::sync::{RwLock, mpsc};
+use ulid::Ulid;
+
+use crate::{
+ alerts::{
+ AlertError, CURRENT_ALERTS_VERSION,
+ alert_enums::{
+ AlertOperator, AlertState, AlertTask, AlertType, AlertVersion, EvalConfig,
+ LogicalOperator, NotificationState, Severity, WhereConfigOperator,
+ },
+ alert_traits::AlertTrait,
+ target::{NotificationConfig, TARGETS},
+ },
+ query::resolve_stream_names,
+};
+
+/// Helper struct for basic alert fields during migration
+pub struct BasicAlertFields {
+ pub id: Ulid,
+ pub title: String,
+ pub severity: Severity,
+}
+
+#[derive(Debug)]
+pub struct Alerts {
+ pub alerts: RwLock>>,
+ pub sender: mpsc::Sender,
+}
+
+#[derive(Debug, Clone)]
+pub struct Context {
+ pub alert_info: AlertInfo,
+ pub deployment_info: DeploymentInfo,
+ pub message: String,
+ pub notification_config: NotificationConfig,
+}
+
+impl Context {
+ pub fn new(
+ alert_info: AlertInfo,
+ deployment_info: DeploymentInfo,
+ notification_config: NotificationConfig,
+ message: String,
+ ) -> Self {
+ Self {
+ alert_info,
+ deployment_info,
+ message,
+ notification_config,
+ }
+ }
+
+ pub(crate) fn default_alert_string(&self) -> String {
+ format!(
+ "AlertName: {}\nTriggered TimeStamp: {}\nSeverity: {}\n{}",
+ self.alert_info.alert_name,
+ Utc::now().to_rfc3339(),
+ self.alert_info.severity,
+ self.message
+ )
+ }
+
+ pub(crate) fn default_resolved_string(&self) -> String {
+ format!("{} is now `not-triggered` ", self.alert_info.alert_name)
+ }
+
+ pub(crate) fn default_disabled_string(&self) -> String {
+ format!(
+ "{} is now `disabled`. No more evals will be run till it is `disabled`.",
+ self.alert_info.alert_name
+ )
+ }
+
+ // fn default_silenced_string(&self) -> String {
+ // format!(
+ // "Notifications for {} have been silenced ",
+ // self.alert_info.alert_name
+ // )
+ // }
+}
+
+#[derive(Debug, Clone)]
+pub struct AlertInfo {
+ pub alert_id: Ulid,
+ pub alert_name: String,
+ // message: String,
+ // reason: String,
+ pub alert_state: AlertState,
+ pub notification_state: NotificationState,
+ pub severity: String,
+}
+
+impl AlertInfo {
+ pub fn new(
+ alert_id: Ulid,
+ alert_name: String,
+ alert_state: AlertState,
+ notification_state: NotificationState,
+ severity: String,
+ ) -> Self {
+ Self {
+ alert_id,
+ alert_name,
+ alert_state,
+ notification_state,
+ severity,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct DeploymentInfo {
+ pub deployment_instance: String,
+ pub deployment_id: Ulid,
+ pub deployment_mode: String,
+}
+
+impl DeploymentInfo {
+ pub fn new(deployment_instance: String, deployment_id: Ulid, deployment_mode: String) -> Self {
+ Self {
+ deployment_instance,
+ deployment_id,
+ deployment_mode,
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+pub struct OperationConfig {
+ pub column: String,
+ pub operator: Option,
+ pub value: Option,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct FilterConfig {
+ pub conditions: Vec,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+pub struct ConditionConfig {
+ pub column: String,
+ pub operator: WhereConfigOperator,
+ pub value: Option,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct Conditions {
+ pub operator: Option,
+ pub condition_config: Vec,
+}
+
+impl Conditions {
+ pub fn generate_filter_message(&self) -> String {
+ match &self.operator {
+ Some(op) => match op {
+ LogicalOperator::And | LogicalOperator::Or => {
+ let expr1 = &self.condition_config[0];
+ let expr2 = &self.condition_config[1];
+ let expr1_msg = if expr1.value.as_ref().is_some_and(|v| !v.is_empty()) {
+ format!(
+ "{} {} {}",
+ expr1.column,
+ expr1.operator,
+ expr1.value.as_ref().unwrap()
+ )
+ } else {
+ format!("{} {}", expr1.column, expr1.operator)
+ };
+
+ let expr2_msg = if expr2.value.as_ref().is_some_and(|v| !v.is_empty()) {
+ format!(
+ "{} {} {}",
+ expr2.column,
+ expr2.operator,
+ expr2.value.as_ref().unwrap()
+ )
+ } else {
+ format!("{} {}", expr2.column, expr2.operator)
+ };
+
+ format!("[{expr1_msg} {op} {expr2_msg}]")
+ }
+ },
+ None => {
+ let expr = &self.condition_config[0];
+ if let Some(val) = &expr.value {
+ format!("{} {} {}", expr.column, expr.operator, val)
+ } else {
+ format!("{} {}", expr.column, expr.operator)
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct GroupBy {
+ pub columns: Vec,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct ThresholdConfig {
+ pub operator: AlertOperator,
+ pub value: f64,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct RollingWindow {
+ // x minutes (25m)
+ pub eval_start: String,
+ // should always be "now"
+ pub eval_end: String,
+ // x minutes (5m)
+ pub eval_frequency: u64,
+}
+
+impl Default for RollingWindow {
+ fn default() -> Self {
+ Self {
+ eval_start: "10m".into(),
+ eval_end: "now".into(),
+ eval_frequency: 10,
+ }
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct AlertRequest {
+ #[serde(default = "Severity::default")]
+ pub severity: Severity,
+ pub title: String,
+ pub query: String,
+ pub alert_type: AlertType,
+ pub threshold_config: ThresholdConfig,
+ #[serde(default)]
+ pub notification_config: NotificationConfig,
+ pub eval_config: EvalConfig,
+ pub targets: Vec,
+ pub tags: Option>,
+}
+
+impl AlertRequest {
+ pub async fn into(self) -> Result {
+ // Validate that all target IDs exist
+ for id in &self.targets {
+ TARGETS.get_target_by_id(id).await?;
+ }
+ let datasets = resolve_stream_names(&self.query)?;
+ let config = AlertConfig {
+ version: AlertVersion::from(CURRENT_ALERTS_VERSION),
+ id: Ulid::new(),
+ severity: self.severity,
+ title: self.title,
+ query: self.query,
+ datasets,
+ alert_type: self.alert_type,
+ threshold_config: self.threshold_config,
+ eval_config: self.eval_config,
+ targets: self.targets,
+ state: AlertState::default(),
+ notification_state: NotificationState::Notify,
+ notification_config: self.notification_config,
+ created: Utc::now(),
+ tags: self.tags,
+ };
+ Ok(config)
+ }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct AlertConfig {
+ pub version: AlertVersion,
+ #[serde(default)]
+ pub id: Ulid,
+ pub severity: Severity,
+ pub title: String,
+ pub query: String,
+ pub datasets: Vec,
+ pub alert_type: AlertType,
+ pub threshold_config: ThresholdConfig,
+ pub eval_config: EvalConfig,
+ pub targets: Vec,
+ // for new alerts, state should be resolved
+ #[serde(default)]
+ pub state: AlertState,
+ pub notification_state: NotificationState,
+ pub notification_config: NotificationConfig,
+ pub created: DateTime,
+ pub tags: Option>,
+}
+
+#[derive(Debug, Serialize)]
+pub struct AlertsSummary {
+ pub total: u64,
+ pub triggered: AlertsInfoByState,
+ pub paused: AlertsInfoByState,
+ pub resolved: AlertsInfoByState,
+}
+
+#[derive(Debug, Serialize)]
+pub struct AlertsInfoByState {
+ pub total: u64,
+ pub alert_info: Vec,
+}
+
+#[derive(Debug, Serialize)]
+pub struct AlertsInfo {
+ pub title: String,
+ pub id: Ulid,
+ pub severity: Severity,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct ForecastConfig {
+ pub historic_duration: String,
+ pub forecast_duration: String,
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct AnomalyConfig {
+ pub historic_duration: String,
+}
+
+/// Result structure for alert query execution with group support
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AlertQueryResult {
+ /// List of group results, each containing group values and the aggregate value
+ pub groups: Vec,
+ /// True if this is a simple query without GROUP BY (single group with empty group_values)
+ pub is_simple_query: bool,
+}
+
+/// Result for a single group in a GROUP BY query
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GroupResult {
+ /// The group-by column values (empty for non-GROUP BY queries)
+ pub group_values: HashMap,
+ /// The aggregate function value for this group
+ pub aggregate_value: f64,
+}
+
+impl AlertQueryResult {
+ /// Get the single aggregate value for simple queries (backward compatibility)
+ pub fn get_single_value(&self) -> f64 {
+ if self.is_simple_query && !self.groups.is_empty() {
+ self.groups[0].aggregate_value
+ } else {
+ 0.0
+ }
+ }
+}
diff --git a/src/alerts/traits.rs b/src/alerts/alert_traits.rs
similarity index 68%
rename from src/alerts/traits.rs
rename to src/alerts/alert_traits.rs
index 303f6d42f..aa1133f82 100644
--- a/src/alerts/traits.rs
+++ b/src/alerts/alert_traits.rs
@@ -18,22 +18,44 @@
use crate::{
alerts::{
- AlertConfig, AlertError, AlertState, AlertType, EvalConfig, Severity, ThresholdConfig,
+ AlertConfig, AlertError, AlertState, AlertType, EvalConfig, Severity,
+ alert_enums::NotificationState,
+ alert_structs::{Context, ThresholdConfig},
},
rbac::map::SessionKey,
};
+use chrono::{DateTime, Utc};
use std::{collections::HashMap, fmt::Debug};
use tonic::async_trait;
use ulid::Ulid;
+/// A trait to handle different types of messages built by different alert types
+pub trait MessageCreation {
+ fn create_threshold_message(&self, actual_value: f64) -> Result;
+ fn create_anomaly_message(
+ &self,
+ actual_value: f64,
+ lower_bound: f64,
+ upper_bound: f64,
+ ) -> Result;
+ fn create_forecast_message(
+ &self,
+ forecasted_time: DateTime,
+ forecasted_value: f64,
+ ) -> Result;
+}
+
#[async_trait]
pub trait AlertTrait: Debug + Send + Sync {
- async fn eval_alert(&self) -> Result<(bool, f64), AlertError>;
+ async fn eval_alert(&self) -> Result