1919use std:: { collections:: HashMap , time:: Duration } ;
2020
2121use chrono:: { DateTime , Utc } ;
22- use serde:: { Deserialize , Serialize } ;
22+ use serde:: { Deserialize , Deserializer , Serialize } ;
23+ use serde_json:: Value ;
2324use tokio:: sync:: { RwLock , mpsc} ;
2425use ulid:: Ulid ;
2526
@@ -38,6 +39,52 @@ use crate::{
3839 storage:: object_storage:: { alert_json_path, alert_state_json_path} ,
3940} ;
4041
42+ /// Custom deserializer for DateTime<Utc> that handles legacy empty strings
43+ ///
44+ /// This is a compatibility layer for migrating old alerts that stored empty strings
45+ /// instead of valid timestamps. In production, this should log warnings to help
46+ /// identify data quality issues.
47+ ///
48+ /// # Migration Path
49+ /// - Empty strings → Default to current time with a warning
50+ /// - Missing fields → Default to current time
51+ /// - Valid timestamps → Parse normally
52+ pub ( crate ) fn deserialize_datetime_with_empty_string_fallback < ' de , D > (
53+ deserializer : D ,
54+ ) -> Result < DateTime < Utc > , D :: Error >
55+ where
56+ D : Deserializer < ' de > ,
57+ {
58+ #[ derive( Deserialize ) ]
59+ #[ serde( untagged) ]
60+ enum DateTimeOrString {
61+ DateTime ( DateTime < Utc > ) ,
62+ String ( String ) ,
63+ }
64+
65+ match DateTimeOrString :: deserialize ( deserializer) ? {
66+ DateTimeOrString :: DateTime ( dt) => Ok ( dt) ,
67+ DateTimeOrString :: String ( s) => {
68+ if s. is_empty ( ) {
69+ // Log warning about data quality issue
70+ tracing:: warn!(
71+ "Alert has empty 'created' field - this indicates a data quality issue. \
72+ Defaulting to current timestamp. Please investigate and fix the data source."
73+ ) ;
74+ Ok ( Utc :: now ( ) )
75+ } else {
76+ s. parse :: < DateTime < Utc > > ( ) . map_err ( serde:: de:: Error :: custom)
77+ }
78+ }
79+ }
80+ }
81+
82+ /// Default function for created timestamp - returns current time
83+ /// This handles the case where created field is missing in deserialization
84+ pub ( crate ) fn default_created_time ( ) -> DateTime < Utc > {
85+ Utc :: now ( )
86+ }
87+
4188/// Helper struct for basic alert fields during migration
4289pub struct BasicAlertFields {
4390 pub id : Ulid ,
@@ -253,6 +300,8 @@ pub struct AlertRequest {
253300 pub eval_config : EvalConfig ,
254301 pub targets : Vec < Ulid > ,
255302 pub tags : Option < Vec < String > > ,
303+ #[ serde( flatten) ]
304+ pub other_fields : Option < serde_json:: Map < String , Value > > ,
256305}
257306
258307impl AlertRequest {
@@ -309,6 +358,7 @@ impl AlertRequest {
309358 created : Utc :: now ( ) ,
310359 tags : self . tags ,
311360 last_triggered_at : None ,
361+ other_fields : self . other_fields ,
312362 } ;
313363 Ok ( config)
314364 }
@@ -333,9 +383,15 @@ pub struct AlertConfig {
333383 pub state : AlertState ,
334384 pub notification_state : NotificationState ,
335385 pub notification_config : NotificationConfig ,
386+ #[ serde(
387+ default = "default_created_time" ,
388+ deserialize_with = "deserialize_datetime_with_empty_string_fallback"
389+ ) ]
336390 pub created : DateTime < Utc > ,
337391 pub tags : Option < Vec < String > > ,
338392 pub last_triggered_at : Option < DateTime < Utc > > ,
393+ #[ serde( flatten) ]
394+ pub other_fields : Option < serde_json:: Map < String , Value > > ,
339395}
340396
341397#[ derive( Debug , serde:: Serialize , serde:: Deserialize , Clone ) ]
@@ -359,9 +415,15 @@ pub struct AlertConfigResponse {
359415 pub state : AlertState ,
360416 pub notification_state : NotificationState ,
361417 pub notification_config : NotificationConfig ,
418+ #[ serde(
419+ default = "default_created_time" ,
420+ deserialize_with = "deserialize_datetime_with_empty_string_fallback"
421+ ) ]
362422 pub created : DateTime < Utc > ,
363423 pub tags : Option < Vec < String > > ,
364424 pub last_triggered_at : Option < DateTime < Utc > > ,
425+ #[ serde( flatten) ]
426+ pub other_fields : Option < serde_json:: Map < String , Value > > ,
365427}
366428
367429impl AlertConfig {
@@ -401,6 +463,7 @@ impl AlertConfig {
401463 created : self . created ,
402464 tags : self . tags ,
403465 last_triggered_at : self . last_triggered_at ,
466+ other_fields : self . other_fields ,
404467 }
405468 }
406469}
0 commit comments