1919use std:: { collections:: HashMap , time:: Duration } ;
2020
2121use chrono:: { DateTime , Utc } ;
22- use serde:: { Deserialize , Deserializer , Serialize } ;
22+ use serde:: { Deserialize , Serialize } ;
2323use serde_json:: Value ;
2424use tokio:: sync:: { RwLock , mpsc} ;
2525use 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+
2742const 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
11072pub struct BasicAlertFields {
11173 pub id : Ulid ,
@@ -328,7 +290,7 @@ pub struct AlertRequest {
328290impl 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
470432impl 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 ,
0 commit comments