Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 1 addition & 85 deletions src/alerts/alert_structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,11 @@
use std::{collections::HashMap, time::Duration};

use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::{RwLock, mpsc};
use ulid::Ulid;

const RESERVED_FIELDS: &[&str] = &[
"id",
"version",
"severity",
"title",
"query",
"datasets",
"alertType",
"anomalyConfig",
"forecastConfig",
"thresholdConfig",
"notificationConfig",
"evalConfig",
"targets",
"tags",
"state",
"notificationState",
"created",
"lastTriggeredAt",
];

use crate::{
alerts::{
AlertError, CURRENT_ALERTS_VERSION,
Expand All @@ -60,52 +39,6 @@ use crate::{
storage::object_storage::{alert_json_path, alert_state_json_path},
};

/// Custom deserializer for DateTime<Utc> that handles legacy empty strings
///
/// This is a compatibility layer for migrating old alerts that stored empty strings
/// instead of valid timestamps. In production, this should log warnings to help
/// identify data quality issues.
///
/// # Migration Path
/// - Empty strings → Default to current time with a warning
/// - Missing fields → Default to current time
/// - Valid timestamps → Parse normally
pub fn deserialize_datetime_with_empty_string_fallback<'de, D>(
deserializer: D,
) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum DateTimeOrString {
DateTime(DateTime<Utc>),
String(String),
}

match DateTimeOrString::deserialize(deserializer)? {
DateTimeOrString::DateTime(dt) => Ok(dt),
DateTimeOrString::String(s) => {
if s.is_empty() {
// Log warning about data quality issue
tracing::warn!(
"Alert has empty 'created' field - this indicates a data quality issue. \
Defaulting to current timestamp. Please investigate and fix the data source."
);
Ok(Utc::now())
} else {
s.parse::<DateTime<Utc>>().map_err(serde::de::Error::custom)
}
}
}
}

/// Default function for created timestamp - returns current time
/// This handles the case where created field is missing in deserialization
pub fn default_created_time() -> DateTime<Utc> {
Utc::now()
}

/// Helper struct for basic alert fields during migration
pub struct BasicAlertFields {
pub id: Ulid,
Expand Down Expand Up @@ -336,15 +269,6 @@ impl AlertRequest {
other_fields.len()
)));
}

for key in other_fields.keys() {
if RESERVED_FIELDS.contains(&key.as_str()) {
return Err(AlertError::ValidationFailure(format!(
"Field '{}' cannot be in other_fields as it's a reserved field name",
key
)));
}
}
}

// Validate that all target IDs exist
Expand Down Expand Up @@ -424,10 +348,6 @@ pub struct AlertConfig {
pub state: AlertState,
pub notification_state: NotificationState,
pub notification_config: NotificationConfig,
#[serde(
default = "default_created_time",
deserialize_with = "deserialize_datetime_with_empty_string_fallback"
)]
pub created: DateTime<Utc>,
pub tags: Option<Vec<String>>,
pub last_triggered_at: Option<DateTime<Utc>>,
Expand Down Expand Up @@ -456,10 +376,6 @@ pub struct AlertConfigResponse {
pub state: AlertState,
pub notification_state: NotificationState,
pub notification_config: NotificationConfig,
#[serde(
default = "default_created_time",
deserialize_with = "deserialize_datetime_with_empty_string_fallback"
)]
pub created: DateTime<Utc>,
pub tags: Option<Vec<String>>,
pub last_triggered_at: Option<DateTime<Utc>>,
Expand Down
9 changes: 1 addition & 8 deletions src/alerts/alert_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ use crate::{
AlertConfig, AlertError, AlertState, AlertType, AlertVersion, EvalConfig, Severity,
ThresholdConfig,
alert_enums::NotificationState,
alert_structs::{
AlertStateEntry, GroupResult, default_created_time,
deserialize_datetime_with_empty_string_fallback,
},
alert_structs::{AlertStateEntry, GroupResult},
alert_traits::{AlertTrait, MessageCreation},
alerts_utils::{evaluate_condition, execute_alert_query, extract_time_range},
get_number_of_agg_exprs,
Expand Down Expand Up @@ -65,10 +62,6 @@ pub struct ThresholdAlert {
pub state: AlertState,
pub notification_state: NotificationState,
pub notification_config: NotificationConfig,
#[serde(
default = "default_created_time",
deserialize_with = "deserialize_datetime_with_empty_string_fallback"
)]
pub created: DateTime<Utc>,
pub tags: Option<Vec<String>>,
pub datasets: Vec<String>,
Expand Down
2 changes: 1 addition & 1 deletion src/alerts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub use crate::alerts::alert_enums::{
pub use crate::alerts::alert_structs::{
AlertConfig, AlertInfo, AlertRequest, AlertStateEntry, Alerts, AlertsInfo, AlertsInfoByState,
AlertsSummary, BasicAlertFields, Context, DeploymentInfo, RollingWindow, StateTransition,
ThresholdConfig, default_created_time, deserialize_datetime_with_empty_string_fallback,
ThresholdConfig,
};
use crate::alerts::alert_traits::{AlertManagerTrait, AlertTrait};
use crate::alerts::alert_types::ThresholdAlert;
Expand Down
Loading