///|
/// Policy for production anomaly alerts.
pub struct AlertPolicy {
warning_threshold : Double
critical_threshold : Double
recovery_threshold : Double
consecutive_count : Int
cooldown : Int
direction : Int
}
///|
/// One alert transition emitted by a monitor.
pub struct AlertEvent {
index : Int
value : Double
score : Double
level : Int
active : Bool
recovered : Bool
}
///|
/// Stateful alert monitor with hysteresis and cooldown.
pub struct AlertMonitor {
policy : AlertPolicy
mut index : Int
mut level : Int
mut consecutive : Int
mut cooldown_left : Int
mut active : Bool
mut events : Int
}
///|
pub fn alert_default_policy() -> AlertPolicy {
{
warning_threshold: 2.5,
critical_threshold: 4.0,
recovery_threshold: 1.5,
consecutive_count: 2,
cooldown: 0,
direction: 0,
}
}
///|
pub fn alert_policy(
warning_threshold : Double,
critical_threshold : Double,
recovery_threshold : Double,
consecutive_count : Int,
cooldown : Int,
direction : Int,
) -> AlertPolicy {
{
warning_threshold: if warning_threshold < 0.0 {
0.0
} else {
warning_threshold
},
critical_threshold: if critical_threshold < warning_threshold {
warning_threshold
} else {
critical_threshold
},
recovery_threshold: if recovery_threshold < 0.0 {
0.0
} else {
recovery_threshold
},
consecutive_count: if consecutive_count < 1 {
1
} else {
consecutive_count
},
cooldown: if cooldown < 0 {
0
} else {
cooldown
},
direction: if direction < -1 {
-1
} else if direction > 1 {
1
} else {
direction
},
}
}
///|
pub fn alert_score(value : Double, center : Double, scale : Double) -> Double {
if scale <= 1.0e-12 {
if value == center {
0.0
} else {
1.0e12
}
} else {
abs_double(value - center) / scale
}
}
///|
pub fn alert_signed_score(
value : Double,
center : Double,
scale : Double,
) -> Double {
if scale <= 1.0e-12 {
0.0
} else {
(value - center) / scale
}
}
///|
pub fn alert_level(score : Double, policy : AlertPolicy) -> Int {
if score >= policy.critical_threshold {
2
} else if score >= policy.warning_threshold {
1
} else {
0
}
}
///|
pub fn alert_direction_allowed(
value : Double,
center : Double,
policy : AlertPolicy,
) -> Bool {
if policy.direction == 0 {
true
} else if policy.direction > 0 {
value >= center
} else {
value <= center
}
}
///|
pub fn alert_flags(
data : Array[Double],
center : Double,
scale : Double,
policy : AlertPolicy,
) -> Array[Bool] {
let result = []
for value in data {
let score = alert_score(value, center, scale)
result.push(
alert_level(score, policy) > 0 &&
alert_direction_allowed(value, center, policy),
)
}
result
}
///|
pub fn alert_levels(
data : Array[Double],
center : Double,
scale : Double,
policy : AlertPolicy,
) -> Array[Int] {
let result = []
for value in data {
let score = alert_score(value, center, scale)
result.push(
if alert_direction_allowed(value, center, policy) {
alert_level(score, policy)
} else {
0
},
)
}
result
}
///|
pub fn alert_debounce(flags : Array[Bool], consecutive : Int) -> Array[Bool] {
let required = if consecutive < 1 { 1 } else { consecutive }
let result = []
let mut run = 0
for flag in flags {
if flag {
run += 1
} else {
run = 0
}
result.push(run >= required)
}
result
}
///|
pub fn alert_recovery_flags(
scores : Array[Double],
policy : AlertPolicy,
) -> Array[Bool] {
let result = []
let mut active = false
for score in scores {
let entered = score >= policy.warning_threshold
let recovered = score <= policy.recovery_threshold
if !active && entered {
active = true
} else if active && recovered {
active = false
}
result.push(active)
}
result
}
///|
pub fn alert_cooldown_flags(flags : Array[Bool], cooldown : Int) -> Array[Bool] {
let wait = if cooldown < 0 { 0 } else { cooldown }
let result = []
let mut remaining = 0
for flag in flags {
if flag && remaining == 0 {
result.push(true)
remaining = wait
} else {
result.push(false)
if remaining > 0 {
remaining -= 1
}
}
}
result
}
///|
pub fn alert_event_indices(flags : Array[Bool]) -> Array[Int] {
let result = []
let mut previous = false
for index = 0; index < flags.length(); index = index + 1 {
if flags[index] && !previous {
result.push(index)
}
previous = flags[index]
}
result
}
///|
pub fn alert_recovery_indices(flags : Array[Bool]) -> Array[Int] {
let result = []
let mut previous = false
for index = 0; index < flags.length(); index = index + 1 {
if !flags[index] && previous {
result.push(index)
}
previous = flags[index]
}
result
}
///|
pub fn alert_duration_series(flags : Array[Bool]) -> Array[Int] {
let result = []
let mut duration = 0
for flag in flags {
if flag {
duration += 1
} else {
duration = 0
}
result.push(duration)
}
result
}
///|
pub fn alert_incident_durations(flags : Array[Bool]) -> Array[Int] {
let result = []
let mut duration = 0
for flag in flags {
if flag {
duration += 1
} else if duration > 0 {
result.push(duration)
duration = 0
}
}
if duration > 0 {
result.push(duration)
}
result
}
///|
pub fn alert_incident_count(flags : Array[Bool]) -> Int {
alert_event_indices(flags).length()
}
///|
pub fn alert_incident_rate(flags : Array[Bool]) -> Double {
if flags.length() == 0 {
0.0
} else {
alert_incident_count(flags).to_double() / flags.length().to_double()
}
}
///|
pub fn alert_active_fraction(flags : Array[Bool]) -> Double {
let mut count = 0
for flag in flags {
if flag {
count += 1
}
}
if flags.length() == 0 {
0.0
} else {
count.to_double() / flags.length().to_double()
}
}
///|
pub fn alert_mean_duration(flags : Array[Bool]) -> Double {
let durations = alert_incident_durations(flags)
if durations.length() == 0 {
0.0
} else {
let values = []
for duration in durations {
values.push(duration.to_double())
}
mean(values)
}
}
///|
pub fn alert_max_duration(flags : Array[Bool]) -> Int {
let durations = alert_incident_durations(flags)
let mut result = 0
for duration in durations {
if duration > result {
result = duration
}
}
result
}
///|
pub fn alert_quality_score(
flags : Array[Bool],
scores : Array[Double],
) -> Double {
let active = alert_active_fraction(flags)
let magnitude = if scores.length() == 0 { 0.0 } else { mean(scores) }
1.0 / (1.0 + active + magnitude)
}
///|
pub fn alert_health_score(
quality_score : Double,
drift_score : Double,
risk_score : Double,
) -> Double {
let penalty = if quality_score < 0.0 { 1.0 } else { 1.0 - quality_score }
let raw = 1.0 - (penalty + drift_score + risk_score) / 3.0
if raw < 0.0 {
0.0
} else if raw > 1.0 {
1.0
} else {
raw
}
}
///|
pub fn alert_health_from_data(
data : Array[Double],
rule : QualityRule,
drift_baseline : Array[Double],
) -> Double {
let quality = quality_report(data, rule).quality_score
let drift = drift_score(drift_baseline, data, 10)
let risk = robust_signal_quality(data)
alert_health_score(quality, drift, risk)
}
///|
pub fn AlertMonitor::new(policy : AlertPolicy) -> AlertMonitor {
{
policy,
index: 0,
level: 0,
consecutive: 0,
cooldown_left: 0,
active: false,
events: 0,
}
}
///|
pub fn AlertMonitor::observe(
self : AlertMonitor,
value : Double,
center : Double,
scale : Double,
) -> AlertEvent {
let score = alert_signed_score(value, center, scale)
let magnitude = abs_double(score)
let candidate = alert_direction_allowed(value, center, self.policy)
let candidate_level = if candidate {
alert_level(magnitude, self.policy)
} else {
0
}
let previous_active = self.active
if candidate_level > 0 {
self.consecutive += 1
} else {
self.consecutive = 0
}
if self.cooldown_left > 0 {
self.cooldown_left -= 1
}
if !self.active &&
candidate_level > 0 &&
self.consecutive >= self.policy.consecutive_count &&
self.cooldown_left == 0 {
self.active = true
self.level = candidate_level
self.events += 1
self.cooldown_left = self.policy.cooldown
} else if self.active && magnitude <= self.policy.recovery_threshold {
self.active = false
self.level = 0
} else if self.active && candidate_level > self.level {
self.level = candidate_level
}
let recovered = previous_active && !self.active
let event = {
index: self.index,
value,
score: magnitude,
level: self.level,
active: self.active,
recovered,
}
self.index += 1
event
}
///|
pub fn AlertMonitor::observe_many(
self : AlertMonitor,
data : Array[Double],
center : Double,
scale : Double,
) -> Array[AlertEvent] {
let result = []
for value in data {
result.push(self.observe(value, center, scale))
}
result
}
///|
pub fn AlertMonitor::reset(self : AlertMonitor) -> Unit {
self.index = 0
self.level = 0
self.consecutive = 0
self.cooldown_left = 0
self.active = false
self.events = 0
}
///|
pub fn AlertMonitor::is_active(self : AlertMonitor) -> Bool {
self.active
}
///|
pub fn AlertMonitor::level(self : AlertMonitor) -> Int {
self.level
}
///|
pub fn AlertMonitor::seen(self : AlertMonitor) -> Int {
self.index
}
///|
pub fn AlertMonitor::event_count(self : AlertMonitor) -> Int {
self.events
}
///|
pub fn alert_score_series(data : Array[Double], window : Int) -> Array[Double] {
let center = rolling_median(data, if window < 2 { 2 } else { window })
let scale = rolling_mad(data, if window < 2 { 2 } else { window })
let result = []
for index = 0; index < data.length(); index = index + 1 {
let local_scale = if scale[index] <= 1.0e-12 {
1.0
} else {
scale[index] * 1.4826
}
result.push(alert_score(data[index], center[index], local_scale))
}
result
}
///|
pub fn alert_series(
data : Array[Double],
window : Int,
policy : AlertPolicy,
) -> Array[AlertEvent] {
let monitor = AlertMonitor::new(policy)
let safe_window = if window < 2 { 2 } else { window }
let center = mean(rolling_median(data, safe_window))
let scale = mad(rolling_mad(data, safe_window)) * 1.4826
monitor.observe_many(data, center, if scale <= 1.0e-12 { 1.0 } else { scale })
}
///|
pub fn alert_series_flags(
data : Array[Double],
window : Int,
policy : AlertPolicy,
) -> Array[Bool] {
let scores = alert_score_series(data, window)
let flags = alert_flags(scores, 0.0, 1.0, policy)
alert_debounce(flags, policy.consecutive_count)
}
///|
pub fn alert_series_levels(
data : Array[Double],
window : Int,
policy : AlertPolicy,
) -> Array[Int] {
let scores = alert_score_series(data, window)
alert_levels(scores, 0.0, 1.0, policy)
}
///|
pub fn alert_slo_breach_fraction(
flags : Array[Bool],
target : Double,
) -> Double {
let actual = alert_active_fraction(flags)
if actual <= target {
0.0
} else {
actual - target
}
}
///|
pub fn alert_slo_score(flags : Array[Bool], target : Double) -> Double {
let breach = alert_slo_breach_fraction(flags, target)
if target <= 0.0 {
if breach == 0.0 {
1.0
} else {
0.0
}
} else {
1.0 - breach / target
}
}
///|
pub fn alert_trend(scores : Array[Double]) -> Double {
if scores.length() < 2 {
0.0
} else {
let x = []
for index = 0; index < scores.length(); index = index + 1 {
x.push(index.to_double())
}
linear_regression(x, scores).slope
}
}
///|
pub fn alert_escalation_indices(levels : Array[Int]) -> Array[Int] {
let result = []
for index = 1; index < levels.length(); index = index + 1 {
if levels[index] > levels[index - 1] {
result.push(index)
}
}
result
}
///|
pub fn alert_recovery_quality(flags : Array[Bool]) -> Double {
let starts = alert_event_indices(flags)
let ends = alert_recovery_indices(flags)
if starts.length() == 0 {
1.0
} else {
ends.length().to_double() / starts.length().to_double()
}
}