Skip to content

Commit d5be869

Browse files
sanitise and remove reserved fields from alert other_fields (#1462)
* sanitise and remove reserved field from other_fields
1 parent 7ebe041 commit d5be869

File tree

3 files changed

+66
-92
lines changed

3 files changed

+66
-92
lines changed

src/alerts/alert_structs.rs

Lines changed: 64 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -19,93 +19,55 @@
1919
use std::{collections::HashMap, time::Duration};
2020

2121
use chrono::{DateTime, Utc};
22-
use serde::{Deserialize, Deserializer, Serialize};
22+
use serde::{Deserialize, Serialize};
2323
use serde_json::Value;
2424
use tokio::sync::{RwLock, mpsc};
2525
use ulid::Ulid;
2626

27+
use crate::{
28+
alerts::{
29+
AlertError, CURRENT_ALERTS_VERSION,
30+
alert_enums::{
31+
AlertOperator, AlertState, AlertTask, AlertType, AlertVersion, EvalConfig,
32+
LogicalOperator, NotificationState, Severity, WhereConfigOperator,
33+
},
34+
alert_traits::AlertTrait,
35+
target::{NotificationConfig, TARGETS},
36+
},
37+
metastore::metastore_traits::MetastoreObject,
38+
query::resolve_stream_names,
39+
storage::object_storage::{alert_json_path, alert_state_json_path},
40+
};
41+
2742
const RESERVED_FIELDS: &[&str] = &[
28-
"id",
2943
"version",
44+
"id",
3045
"severity",
3146
"title",
3247
"query",
3348
"datasets",
3449
"alertType",
50+
"alert_type",
3551
"anomalyConfig",
52+
"anomaly_config",
3653
"forecastConfig",
54+
"forecast_config",
3755
"thresholdConfig",
38-
"notificationConfig",
56+
"threshold_config",
3957
"evalConfig",
58+
"eval_config",
4059
"targets",
41-
"tags",
4260
"state",
4361
"notificationState",
62+
"notification_state",
63+
"notificationConfig",
64+
"notification_config",
4465
"created",
66+
"tags",
4567
"lastTriggeredAt",
68+
"last_triggered_at",
4669
];
4770

48-
use crate::{
49-
alerts::{
50-
AlertError, CURRENT_ALERTS_VERSION,
51-
alert_enums::{
52-
AlertOperator, AlertState, AlertTask, AlertType, AlertVersion, EvalConfig,
53-
LogicalOperator, NotificationState, Severity, WhereConfigOperator,
54-
},
55-
alert_traits::AlertTrait,
56-
target::{NotificationConfig, TARGETS},
57-
},
58-
metastore::metastore_traits::MetastoreObject,
59-
query::resolve_stream_names,
60-
storage::object_storage::{alert_json_path, alert_state_json_path},
61-
};
62-
63-
/// Custom deserializer for DateTime<Utc> that handles legacy empty strings
64-
///
65-
/// This is a compatibility layer for migrating old alerts that stored empty strings
66-
/// instead of valid timestamps. In production, this should log warnings to help
67-
/// identify data quality issues.
68-
///
69-
/// # Migration Path
70-
/// - Empty strings → Default to current time with a warning
71-
/// - Missing fields → Default to current time
72-
/// - Valid timestamps → Parse normally
73-
pub fn deserialize_datetime_with_empty_string_fallback<'de, D>(
74-
deserializer: D,
75-
) -> Result<DateTime<Utc>, D::Error>
76-
where
77-
D: Deserializer<'de>,
78-
{
79-
#[derive(Deserialize)]
80-
#[serde(untagged)]
81-
enum DateTimeOrString {
82-
DateTime(DateTime<Utc>),
83-
String(String),
84-
}
85-
86-
match DateTimeOrString::deserialize(deserializer)? {
87-
DateTimeOrString::DateTime(dt) => Ok(dt),
88-
DateTimeOrString::String(s) => {
89-
if s.is_empty() {
90-
// Log warning about data quality issue
91-
tracing::warn!(
92-
"Alert has empty 'created' field - this indicates a data quality issue. \
93-
Defaulting to current timestamp. Please investigate and fix the data source."
94-
);
95-
Ok(Utc::now())
96-
} else {
97-
s.parse::<DateTime<Utc>>().map_err(serde::de::Error::custom)
98-
}
99-
}
100-
}
101-
}
102-
103-
/// Default function for created timestamp - returns current time
104-
/// This handles the case where created field is missing in deserialization
105-
pub fn default_created_time() -> DateTime<Utc> {
106-
Utc::now()
107-
}
108-
10971
/// Helper struct for basic alert fields during migration
11072
pub struct BasicAlertFields {
11173
pub id: Ulid,
@@ -328,7 +290,7 @@ pub struct AlertRequest {
328290
impl AlertRequest {
329291
pub async fn into(self) -> Result<AlertConfig, AlertError> {
330292
// Validate that other_fields doesn't contain reserved field names
331-
if let Some(ref other_fields) = self.other_fields {
293+
let other_fields = if let Some(mut other_fields) = self.other_fields {
332294
// Limit other_fields to maximum 10 fields
333295
if other_fields.len() > 10 {
334296
return Err(AlertError::ValidationFailure(format!(
@@ -337,15 +299,20 @@ impl AlertRequest {
337299
)));
338300
}
339301

340-
for key in other_fields.keys() {
341-
if RESERVED_FIELDS.contains(&key.as_str()) {
342-
return Err(AlertError::ValidationFailure(format!(
343-
"Field '{}' cannot be in other_fields as it's a reserved field name",
344-
key
345-
)));
302+
for reserved in RESERVED_FIELDS {
303+
if other_fields.remove(*reserved).is_some() {
304+
tracing::warn!("Removed reserved field '{}' from other_fields", reserved);
346305
}
347306
}
348-
}
307+
308+
if other_fields.is_empty() {
309+
None
310+
} else {
311+
Some(other_fields)
312+
}
313+
} else {
314+
None
315+
};
349316

350317
// Validate that all target IDs exist
351318
for id in &self.targets {
@@ -359,6 +326,8 @@ impl AlertRequest {
359326
)));
360327
}
361328

329+
let created_timestamp = Utc::now();
330+
362331
let config = AlertConfig {
363332
version: AlertVersion::from(CURRENT_ALERTS_VERSION),
364333
id: Ulid::new(),
@@ -396,11 +365,12 @@ impl AlertRequest {
396365
state: AlertState::default(),
397366
notification_state: NotificationState::Notify,
398367
notification_config: self.notification_config,
399-
created: Utc::now(),
368+
created: created_timestamp,
400369
tags: self.tags,
401370
last_triggered_at: None,
402-
other_fields: self.other_fields,
371+
other_fields,
403372
};
373+
404374
Ok(config)
405375
}
406376
}
@@ -424,10 +394,6 @@ pub struct AlertConfig {
424394
pub state: AlertState,
425395
pub notification_state: NotificationState,
426396
pub notification_config: NotificationConfig,
427-
#[serde(
428-
default = "default_created_time",
429-
deserialize_with = "deserialize_datetime_with_empty_string_fallback"
430-
)]
431397
pub created: DateTime<Utc>,
432398
pub tags: Option<Vec<String>>,
433399
pub last_triggered_at: Option<DateTime<Utc>>,
@@ -456,10 +422,6 @@ pub struct AlertConfigResponse {
456422
pub state: AlertState,
457423
pub notification_state: NotificationState,
458424
pub notification_config: NotificationConfig,
459-
#[serde(
460-
default = "default_created_time",
461-
deserialize_with = "deserialize_datetime_with_empty_string_fallback"
462-
)]
463425
pub created: DateTime<Utc>,
464426
pub tags: Option<Vec<String>>,
465427
pub last_triggered_at: Option<DateTime<Utc>>,
@@ -468,6 +430,25 @@ pub struct AlertConfigResponse {
468430
}
469431

470432
impl AlertConfig {
433+
/// Filters out reserved field names from other_fields
434+
/// This prevents conflicts when flattening other_fields during serialization
435+
pub fn sanitize_other_fields(&mut self) {
436+
if let Some(ref mut other_fields) = self.other_fields {
437+
for reserved in RESERVED_FIELDS {
438+
if other_fields.remove(*reserved).is_some() {
439+
tracing::warn!(
440+
"Removed reserved field '{}' from other_fields during sanitization",
441+
reserved
442+
);
443+
}
444+
}
445+
446+
if other_fields.is_empty() {
447+
self.other_fields = None;
448+
}
449+
}
450+
}
451+
471452
pub fn to_response(self) -> AlertConfigResponse {
472453
AlertConfigResponse {
473454
version: self.version,

src/alerts/alert_types.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ use crate::{
2929
AlertConfig, AlertError, AlertState, AlertType, AlertVersion, EvalConfig, Severity,
3030
ThresholdConfig,
3131
alert_enums::NotificationState,
32-
alert_structs::{
33-
AlertStateEntry, GroupResult, default_created_time,
34-
deserialize_datetime_with_empty_string_fallback,
35-
},
32+
alert_structs::{AlertStateEntry, GroupResult},
3633
alert_traits::{AlertTrait, MessageCreation},
3734
alerts_utils::{evaluate_condition, execute_alert_query, extract_time_range},
3835
get_number_of_agg_exprs,
@@ -65,10 +62,6 @@ pub struct ThresholdAlert {
6562
pub state: AlertState,
6663
pub notification_state: NotificationState,
6764
pub notification_config: NotificationConfig,
68-
#[serde(
69-
default = "default_created_time",
70-
deserialize_with = "deserialize_datetime_with_empty_string_fallback"
71-
)]
7265
pub created: DateTime<Utc>,
7366
pub tags: Option<Vec<String>>,
7467
pub datasets: Vec<String>,

src/alerts/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub use crate::alerts::alert_enums::{
5151
pub use crate::alerts::alert_structs::{
5252
AlertConfig, AlertInfo, AlertRequest, AlertStateEntry, Alerts, AlertsInfo, AlertsInfoByState,
5353
AlertsSummary, BasicAlertFields, Context, DeploymentInfo, RollingWindow, StateTransition,
54-
ThresholdConfig, default_created_time, deserialize_datetime_with_empty_string_fallback,
54+
ThresholdConfig,
5555
};
5656
use crate::alerts::alert_traits::{AlertManagerTrait, AlertTrait};
5757
use crate::alerts::alert_types::ThresholdAlert;

0 commit comments

Comments
 (0)