@@ -13,6 +13,7 @@ use tracing::{error, info, warn};
1313use crate :: config:: { Config , Kind , Monitor } ;
1414use crate :: notifications:: Notifiers ;
1515use crate :: probe:: Outcome ;
16+ use crate :: topology;
1617use crate :: { db, probe} ;
1718
1819/// The level a monitor was most recently alerted at, so we never re-alert the
@@ -111,12 +112,18 @@ async fn run(
111112 consecutive_degraded = 0 ;
112113 if consecutive_down >= threshold && alerted != AlertLevel :: Down {
113114 error ! ( monitor = %monitor. id, failures = consecutive_down, "confirmed down" ) ;
115+ let snapshot = config. borrow ( ) . clone ( ) ;
116+ let ( cause, impacted_names) =
117+ down_context ( & snapshot, & pool, & monitor, threshold) . await ;
118+ let impacted_refs: Vec < & str > = impacted_names. iter ( ) . map ( String :: as_str) . collect ( ) ;
114119 notifier
115120 . load_full ( )
116121 . dispatch (
117122 Event :: Down {
118123 monitor : & monitor. name ,
119124 error : outcome. error . as_deref ( ) ,
125+ cause : cause. as_deref ( ) ,
126+ impacted : & impacted_refs,
120127 } ,
121128 monitor. notify . as_deref ( ) ,
122129 )
@@ -169,6 +176,39 @@ async fn heartbeat_outcome(pool: &SqlitePool, monitor: &Monitor) -> Option<Outco
169176 heartbeat_outcome_for ( pool, & monitor. id , monitor. interval_secs ) . await
170177}
171178
179+ /// Compute topology annotation for a down alert: the nearest down upstream
180+ /// (`cause`) if any, or the list of impacted dependents (`impacted`) if this
181+ /// monitor is a root cause. Returns `(None, vec![])` when the monitor has no
182+ /// topology configured.
183+ async fn down_context (
184+ config : & Config ,
185+ pool : & SqlitePool ,
186+ monitor : & Monitor ,
187+ threshold : u32 ,
188+ ) -> ( Option < String > , Vec < String > ) {
189+ let threshold_i64 = i64:: from ( threshold. max ( 1 ) ) ;
190+
191+ let upstreams = topology:: transitive_upstreams ( & config. monitors , & monitor. id ) ;
192+ for up_id in & upstreams {
193+ let Ok ( recent) = db:: recent_checks ( pool, up_id, threshold_i64) . await else {
194+ continue ;
195+ } ;
196+ if db:: derive_status ( & recent, threshold_i64) == "down"
197+ && let Some ( name) = topology:: monitor_name ( & config. monitors , up_id)
198+ {
199+ return ( Some ( name. to_owned ( ) ) , Vec :: new ( ) ) ;
200+ }
201+ }
202+
203+ let dependents = topology:: transitive_dependents ( & config. monitors , & monitor. id ) ;
204+ let impacted: Vec < String > = dependents
205+ . iter ( )
206+ . filter_map ( |dep_id| topology:: monitor_name ( & config. monitors , dep_id) . map ( String :: from) )
207+ . collect ( ) ;
208+
209+ ( None , impacted)
210+ }
211+
172212/// Evaluate a heartbeat from the stored pings for `id` against `interval_secs`:
173213/// down (and record it) when none arrived within the interval, up when one did.
174214/// `None` means no heartbeat yet (or a read error), leaving the status unknown -
0 commit comments