///|
/// Detector selection for the production stack.
pub(all) enum ProductionDetectorKind {
CusumStackDetector
RobustZStackDetector
EwmaStackDetector
VarianceStackDetector
TrendStackDetector
SeasonalStackDetector
DistributionStackDetector
}
///|
pub fn production_detector_kind_name(kind : ProductionDetectorKind) -> String {
match kind {
CusumStackDetector => "cusum"
RobustZStackDetector => "robust-z"
EwmaStackDetector => "ewma"
VarianceStackDetector => "variance"
TrendStackDetector => "trend"
SeasonalStackDetector => "seasonal"
DistributionStackDetector => "distribution"
}
}
///|
/// One weighted vote from a detector stack.
pub struct ProductionDetectorVote {
kind : ProductionDetectorKind
result : DetectionResult
weight : Double
accepted : Bool
}
///|
pub fn ProductionDetectorVote::kind(
self : ProductionDetectorVote,
) -> ProductionDetectorKind {
self.kind
}
///|
pub fn ProductionDetectorVote::result(
self : ProductionDetectorVote,
) -> DetectionResult {
self.result
}
///|
pub fn ProductionDetectorVote::weight(self : ProductionDetectorVote) -> Double {
self.weight
}
///|
pub fn ProductionDetectorVote::accepted(self : ProductionDetectorVote) -> Bool {
self.accepted
}
///|
/// Result of a weighted, explainable ensemble pass.
pub struct ProductionStackResult {
result : DetectionResult
votes : Array[ProductionDetectorVote]
agreement : Double
strongest : ProductionDetectorKind
}
///|
pub fn ProductionStackResult::result(
self : ProductionStackResult,
) -> DetectionResult {
self.result
}
///|
pub fn ProductionStackResult::votes(
self : ProductionStackResult,
) -> Array[ProductionDetectorVote] {
let result : Array[ProductionDetectorVote] = []
for vote in self.votes {
result.push(vote)
}
result
}
///|
pub fn ProductionStackResult::agreement(self : ProductionStackResult) -> Double {
self.agreement
}
///|
pub fn ProductionStackResult::strongest(
self : ProductionStackResult,
) -> ProductionDetectorKind {
self.strongest
}
///|
pub fn ProductionStackResult::summary(self : ProductionStackResult) -> String {
"changed=" +
self.result.changed.to_string() +
",score=" +
self.result.score.to_string() +
",agreement=" +
self.agreement.to_string() +
",strongest=" +
production_detector_kind_name(self.strongest)
}
///|
/// An array-backed ensemble that keeps detector order stable across targets.
pub struct ProductionDetectorStack {
kinds : Array[ProductionDetectorKind]
detectors : Array[PipelineDetector]
weights : Array[Double]
minimum_votes : Int
mut index : Int
}
///|
pub fn ProductionDetectorStack::new(
kinds : Array[ProductionDetectorKind],
detectors : Array[PipelineDetector],
weights? : Array[Double] = [],
minimum_votes? : Int = 1,
) -> ProductionDetectorStack {
let n = if kinds.length() < detectors.length() {
kinds.length()
} else {
detectors.length()
}
let safe_votes = if minimum_votes < 1 {
1
} else if minimum_votes > n {
n
} else {
minimum_votes
}
let normalized : Array[Double] = []
for i in 0.. 0.0 {
weights[i]
} else {
1.0
},
)
}
{
kinds: kinds[:n].to_owned(),
detectors: detectors[:n].to_owned(),
weights: normalized,
minimum_votes: safe_votes,
index: 0,
}
}
///|
pub fn ProductionDetectorStack::length(self : ProductionDetectorStack) -> Int {
self.detectors.length()
}
///|
pub fn ProductionDetectorStack::index(self : ProductionDetectorStack) -> Int {
self.index
}
///|
pub fn ProductionDetectorStack::update(
self : ProductionDetectorStack,
value : Double,
) -> ProductionStackResult {
self.index += 1
let votes : Array[ProductionDetectorVote] = []
let mut total_weight = 0.0
let mut changed_weight = 0.0
let mut score_total = 0.0
let mut confidence_total = 0.0
let mut strongest = if self.kinds.length() == 0 {
CusumStackDetector
} else {
self.kinds[0]
}
let mut strongest_score = 0.0
let mut positive_count = 0
for i in 0..= 1.0
if accepted {
positive_count += 1
}
if result.score > strongest_score {
strongest_score = result.score
strongest = self.kinds[i]
}
total_weight += weight
if accepted {
changed_weight += weight
}
score_total += result.score * weight
confidence_total += result.confidence * weight
votes.push({ kind: self.kinds[i], result, weight, accepted })
}
let safe_weight = if total_weight < 1.0e-12 { 1.0 } else { total_weight }
let agreement = changed_weight / safe_weight
let changed = positive_count >= self.minimum_votes
let result = DetectionResult::new(
changed,
score_total / safe_weight,
confidence_total / safe_weight,
Unknown,
self.index,
evidence=agreement,
)
{ result, votes, agreement, strongest }
}
///|
pub fn ProductionDetectorStack::reset(self : ProductionDetectorStack) -> Unit {
self.index = 0
}
///|
/// Baseline-relative detector for services whose normal level changes gradually.
pub struct ProductionAdaptiveBaselineDetector {
window : DoubleWindow
learning_rate : Double
threshold : Double
warmup : Int
mut baseline : Double
mut index : Int
mut initialized : Bool
}
///|
pub fn ProductionAdaptiveBaselineDetector::new(
window_size? : Int = 32,
learning_rate? : Double = 0.05,
threshold? : Double = 3.0,
warmup? : Int = 8,
) -> ProductionAdaptiveBaselineDetector {
{
window: DoubleWindow::new(if window_size < 2 { 2 } else { window_size }),
learning_rate: clamp_probability(learning_rate),
threshold: if threshold <= 0.0 {
3.0
} else {
threshold
},
warmup: if warmup < 1 {
1
} else {
warmup
},
baseline: 0.0,
index: 0,
initialized: false,
}
}
///|
pub fn ProductionAdaptiveBaselineDetector::baseline(
self : ProductionAdaptiveBaselineDetector,
) -> Double {
self.baseline
}
///|
pub fn ProductionAdaptiveBaselineDetector::index(
self : ProductionAdaptiveBaselineDetector,
) -> Int {
self.index
}
///|
pub fn ProductionAdaptiveBaselineDetector::update(
self : ProductionAdaptiveBaselineDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
if !self.initialized {
ignore(self.window.push(value))
self.baseline = self.window.mean()
if self.window.length() >= self.warmup {
self.initialized = true
}
return DetectionResult::quiet(index=self.index)
}
let residual = value - self.baseline
let scale = self.window.standard_deviation()
let safe_scale = if scale < 1.0e-12 { 1.0 } else { scale }
let z = absolute(residual) / safe_scale
let result = DetectionResult::new(
z >= self.threshold,
z / self.threshold,
clamp_probability(z / (z + 1.0)),
if residual > 0.0 {
Increase
} else if residual < 0.0 {
Decrease
} else {
Unknown
},
self.index,
evidence=residual,
)
self.baseline += self.learning_rate * residual
ignore(self.window.push(value))
result
}
///|
pub fn ProductionAdaptiveBaselineDetector::reset(
self : ProductionAdaptiveBaselineDetector,
) -> Unit {
self.window.clear()
self.baseline = 0.0
self.index = 0
self.initialized = false
}
///|
/// A finite alert budget measured over a rolling event-time interval.
pub struct ProductionEventBudget {
capacity : Int
interval : Int64
mut window_start : Int64?
mut used : Int
mut denied : Int
}
///|
pub fn ProductionEventBudget::new(
capacity? : Int = 20,
interval? : Int64 = 60L,
) -> ProductionEventBudget {
{
capacity: if capacity < 1 {
1
} else {
capacity
},
interval: if interval < 1L {
1L
} else {
interval
},
window_start: None,
used: 0,
denied: 0,
}
}
///|
pub fn ProductionEventBudget::allow(
self : ProductionEventBudget,
timestamp : Int64,
) -> Bool {
match self.window_start {
None => self.window_start = Some(timestamp)
Some(start) =>
if timestamp - start >= self.interval {
self.window_start = Some(timestamp)
self.used = 0
}
}
if self.used < self.capacity {
self.used += 1
true
} else {
self.denied += 1
false
}
}
///|
pub fn ProductionEventBudget::used(self : ProductionEventBudget) -> Int {
self.used
}
///|
pub fn ProductionEventBudget::denied(self : ProductionEventBudget) -> Int {
self.denied
}
///|
pub fn ProductionEventBudget::remaining(self : ProductionEventBudget) -> Int {
if self.capacity > self.used {
self.capacity - self.used
} else {
0
}
}