1use crate::chaos::ChaosController;
2use crate::observation::{
3 generate_observation_id, now_rfc3339, NoopSink, ReliabilityObservation, SharedSink,
4};
5use crate::sla::SlaTracker;
6use std::sync::Arc;
7use std::sync::Mutex;
8
9pub struct BranchMonitor {
10 node_id: String,
11 sla_tracker: Arc<Mutex<SlaTracker>>,
12 chaos: Arc<Mutex<ChaosController>>,
13 observation_sink: SharedSink,
14 service: Option<String>,
15 service_version: Option<String>,
16 deployment: Option<String>,
17 build_ref: Option<String>,
18}
19
20impl BranchMonitor {
21 pub fn new(node_id: &str) -> Self {
22 BranchMonitor {
23 node_id: node_id.to_string(),
24 sla_tracker: Arc::new(Mutex::new(SlaTracker::new())),
25 chaos: Arc::new(Mutex::new(ChaosController::new())),
26 observation_sink: Arc::new(NoopSink),
27 service: None,
28 service_version: None,
29 deployment: None,
30 build_ref: None,
31 }
32 }
33
34 pub fn with_sink(mut self, sink: SharedSink) -> Self {
36 self.observation_sink = sink;
37 self
38 }
39
40 pub fn with_service(mut self, service: impl Into<String>) -> Self {
43 self.service = Some(service.into());
44 self
45 }
46
47 pub fn with_service_version(mut self, version: impl Into<String>) -> Self {
51 self.service_version = Some(version.into());
52 self
53 }
54
55 pub fn with_deployment(mut self, deployment: impl Into<String>) -> Self {
57 self.deployment = Some(deployment.into());
58 self
59 }
60
61 pub fn with_build_ref(mut self, build_ref: impl Into<String>) -> Self {
65 self.build_ref = Some(build_ref.into());
66 self
67 }
68
69 fn observation(&self, outcome: &str) -> ReliabilityObservation {
71 ReliabilityObservation {
72 id: generate_observation_id(),
73 event: self.node_id.clone(),
74 timestamp: now_rfc3339(),
75 service: self.service.clone(),
76 operation: None,
77 environment: None,
78 deployment: self.deployment.clone(),
79 service_version: self.service_version.clone(),
80 build_ref: self.build_ref.clone(),
81 outcome: outcome.to_string(),
82 conditions: Vec::new(),
83 duration_ms: None,
84 trace_id: None,
85 }
86 }
87
88 pub fn record_failure_observation(&self, observation: ReliabilityObservation) {
91 self.observation_sink.emit(&observation);
92 }
93
94 pub fn with_sla(node_id: &str, sla_tracker: Arc<Mutex<SlaTracker>>) -> Self {
95 BranchMonitor {
96 node_id: node_id.to_string(),
97 sla_tracker,
98 chaos: Arc::new(Mutex::new(ChaosController::new())),
99 observation_sink: Arc::new(NoopSink),
100 service: None,
101 service_version: None,
102 deployment: None,
103 build_ref: None,
104 }
105 }
106
107 pub fn with_chaos(node_id: &str, chaos: Arc<Mutex<ChaosController>>) -> Self {
108 BranchMonitor {
109 node_id: node_id.to_string(),
110 sla_tracker: Arc::new(Mutex::new(SlaTracker::new())),
111 chaos,
112 observation_sink: Arc::new(NoopSink),
113 service: None,
114 service_version: None,
115 deployment: None,
116 build_ref: None,
117 }
118 }
119
120 pub fn record_branch(&mut self, outcome: &str, declared_probability: f64) {
121 let should_chaos = {
122 let mut chaos = self.chaos.lock().unwrap();
123 chaos.should_inject_chaos(&self.node_id)
124 };
125 if should_chaos {
126 return;
127 }
128
129 let mut sla = self.sla_tracker.lock().unwrap();
130 let anomaly = sla.record(&self.node_id, outcome, declared_probability, true);
131
132 if anomaly {
133 crate::telemetry::emit_anomaly_event(
134 &self.node_id,
135 outcome,
136 declared_probability,
137 sla.observed_frequency(&self.node_id, outcome),
138 );
139 }
140 drop(sla);
141
142 crate::telemetry::attach_node_span_attribute(&self.node_id);
143
144 self.observation_sink.emit(&self.observation(outcome));
150 }
151
152 pub fn record_failure(
153 &mut self,
154 operation_id: &str,
155 error: &dyn std::error::Error,
156 declared_probability: Option<f64>,
157 ) {
158 let key = format!("{}.failure", operation_id);
159 let outcome = "FAILURE";
160
161 if let Some(prob) = declared_probability {
162 let mut sla = self.sla_tracker.lock().unwrap();
163 let anomaly = sla.record(&key, outcome, prob, true);
164
165 if anomaly {
166 crate::telemetry::emit_anomaly_event(
167 &key,
168 outcome,
169 prob,
170 sla.observed_frequency(&key, outcome),
171 );
172 }
173 }
174
175 crate::telemetry::attach_node_span_attribute(&key);
176 eprintln!("[etdl] operation '{}' failed: {}", operation_id, error);
177
178 let mut o = self.observation("failed");
179 o.event = key;
180 o.operation = Some(operation_id.to_string());
181 self.observation_sink.emit(&o);
182 }
183
184 pub fn record_success(&mut self, operation_id: &str, declared_probability: Option<f64>) {
198 let key = format!("{}.failure", operation_id);
199 let outcome = "FAILURE";
200
201 if let Some(prob) = declared_probability {
202 let mut sla = self.sla_tracker.lock().unwrap();
203 let anomaly = sla.record(&key, outcome, prob, false);
204
205 if anomaly {
206 crate::telemetry::emit_anomaly_event(
207 &key,
208 outcome,
209 prob,
210 sla.observed_frequency(&key, outcome),
211 );
212 }
213 }
214 }
215
216 pub fn flush(&self) {
217 let sla = self.sla_tracker.lock().unwrap();
218 eprintln!(
219 "[etdl] node '{}': {} evaluations recorded",
220 self.node_id,
221 sla.total_evaluations()
222 );
223 }
224}
225
226impl Drop for BranchMonitor {
227 fn drop(&mut self) {
228 self.flush();
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_record_branch() {
238 let mut monitor = BranchMonitor::new("test_barrier");
239 monitor.record_branch("SUCCESS", 0.95);
240 monitor.record_branch("SUCCESS", 0.95);
241 monitor.record_branch("FAILURE", 0.05);
242 }
243
244 #[derive(Debug)]
245 struct FakeError;
246 impl std::fmt::Display for FakeError {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 write!(f, "fake error")
249 }
250 }
251 impl std::error::Error for FakeError {}
252
253 #[test]
263 fn record_success_keeps_observed_frequency_meaningful_not_permanently_one() {
264 let mut monitor = BranchMonitor::new("op");
265 for _ in 0..19 {
268 monitor.record_success("checkout", Some(0.05));
269 }
270 monitor.record_failure("checkout", &FakeError, Some(0.05));
271
272 let observed = monitor
273 .sla_tracker
274 .lock()
275 .unwrap()
276 .observed_frequency("checkout.failure", "FAILURE");
277
278 assert!(
282 (observed - 0.05).abs() < 1e-9,
283 "expected observed frequency ~0.05 (1 failure in 20 attempts), got {observed} \
284 (1.0 would mean the false-alarm bug regressed)"
285 );
286 }
287}