1use super::config::DiagnosticData;
6use super::metrics::fetch::{Fetcher, FileDataFetcher};
7use super::metrics::metric_value::{MetricValue, Problem};
8use super::metrics::{MetricState, Metrics, ValueSource};
9use super::plugins::{register_plugins, Plugin};
10use crate::act::{Action, Actions, Alert, Gauge, Snapshot};
11use crate::inspect_logger;
12use serde::Serialize;
13use std::collections::HashMap;
14
15#[derive(Clone, Debug, Serialize)]
17pub struct TriageOutput {
18 actions: Actions,
19 metrics: Metrics,
20 plugin_results: HashMap<String, Vec<Action>>,
21 triage_errors: Vec<String>,
22}
23
24impl TriageOutput {
25 pub fn new(namespaces: Vec<String>) -> TriageOutput {
27 let mut actions: Actions = HashMap::new();
28 let mut metrics: HashMap<String, HashMap<String, ValueSource>> = HashMap::new();
29
30 for namespace in namespaces {
33 metrics.insert(namespace.clone(), HashMap::new());
34 actions.insert(namespace, HashMap::new());
35 }
36
37 TriageOutput { actions, metrics, triage_errors: Vec::new(), plugin_results: HashMap::new() }
38 }
39
40 pub fn add_error(&mut self, error: String) {
42 self.triage_errors.push(error);
43 }
44
45 pub fn add_action(&mut self, namespace: String, name: String, action: Action) {
47 if let Some(actions_map) = self.actions.get_mut(&namespace) {
48 actions_map.insert(name, action);
49 }
50 }
51
52 pub fn has_reportable_issues(&self) -> bool {
55 for actions_map in self.actions.values() {
56 for action in actions_map.values() {
57 if action.has_reportable_issue() {
58 return true;
59 }
60 }
61 }
62 false
63 }
64}
65
66pub struct StructuredActionContext<'a> {
67 actions: &'a Actions,
68 metric_state: MetricState<'a>,
69 triage_output: TriageOutput,
70 plugins: Vec<Box<dyn Plugin>>,
71}
72
73impl<'a> StructuredActionContext<'a> {
74 pub(crate) fn new(
75 metrics: &'a Metrics,
76 actions: &'a Actions,
77 diagnostic_data: &'a [DiagnosticData],
78 now: Option<i64>,
79 ) -> StructuredActionContext<'a> {
80 let fetcher = FileDataFetcher::new(diagnostic_data);
81 let mut triage_output = TriageOutput::new(metrics.keys().cloned().collect::<Vec<String>>());
82 fetcher.errors().iter().for_each(|e| {
83 triage_output.add_error(format!("[DEBUG: BAD DATA] {}", e));
84 });
85
86 StructuredActionContext {
87 actions,
88 metric_state: MetricState::new(metrics, Fetcher::FileData(fetcher), now),
89 triage_output,
90 plugins: register_plugins(),
91 }
92 }
93}
94
95impl StructuredActionContext<'_> {
96 pub fn process(&mut self) -> &TriageOutput {
100 for (namespace, actions) in self.actions.iter() {
101 for (name, action) in actions.iter() {
102 match action {
103 Action::Alert(alert) => self.update_alerts(alert, namespace, name),
104 Action::Gauge(gauge) => self.update_gauges(gauge, namespace, name),
105 Action::Snapshot(snapshot) => self.update_snapshots(snapshot, namespace, name),
106 };
107 }
108 }
109
110 self.metric_state.evaluate_all_metrics();
111
112 self.triage_output.metrics = self.metric_state.metrics.clone();
113
114 if let Fetcher::FileData(file_data) = &self.metric_state.fetcher {
115 for plugin in &self.plugins {
116 let actions = plugin.run_structured(file_data);
117 self.triage_output.plugin_results.insert(plugin.name().to_string(), actions);
118 }
119 }
120
121 &self.triage_output
122 }
123
124 fn update_alerts(&mut self, action: &Alert, namespace: &str, name: &str) {
126 self.metric_state.eval_action_metric(namespace, &action.trigger);
127 self.triage_output.add_action(
128 namespace.to_owned(),
129 name.to_owned(),
130 Action::Alert(action.clone()),
131 );
132 }
133
134 fn update_snapshots(&mut self, action: &Snapshot, namespace: &str, name: &str) {
136 let snapshot_trigger = self.metric_state.eval_action_metric(namespace, &action.trigger);
137 self.metric_state.eval_action_metric(namespace, &action.repeat);
138 self.triage_output.add_action(
139 namespace.to_string(),
140 name.to_string(),
141 Action::Snapshot(action.clone()),
142 );
143 match snapshot_trigger {
144 MetricValue::Bool(true) => {}
145 MetricValue::Bool(false) => {}
146 MetricValue::Problem(Problem::Ignore(_)) => {}
147 MetricValue::Problem(_reason) => {
148 inspect_logger::log_warn(
149 "Snapshot trigger not boolean",
150 namespace,
151 name,
152 &format!("{:?}", _reason),
153 );
154 }
155 other => {
156 inspect_logger::log_error(
157 "Bad config: Unexpected value type (need boolean)",
158 namespace,
159 name,
160 &format!("{}", other),
161 );
162 }
163 };
164 }
165
166 fn update_gauges(&mut self, action: &Gauge, namespace: &String, name: &String) {
168 self.metric_state.eval_action_metric(namespace, &action.value);
170 self.triage_output.add_action(
171 namespace.to_string(),
172 name.to_string(),
173 Action::Gauge(action.clone()),
174 )
175 }
176}
177
178#[cfg(test)]
179mod test {
180 use super::*;
181 use crate::act::{ActionsSchema, Severity};
182 use crate::make_metrics;
183 use crate::metrics::{ExpressionContext, Metric};
184 use std::cell::RefCell;
185
186 macro_rules! cast {
187 ($target: expr, $pat: path) => {{
188 if let $pat(a) = $target {
189 a
190 } else {
191 panic!("mismatch variant when cast to {}", stringify!($pat));
192 }
193 }};
194 }
195
196 macro_rules! trigger_eq {
197 ($results: expr, $file: expr, $name: expr, $eval: expr) => {{
198 assert_eq!(
199 cast!($results.actions.get($file).unwrap().get($name).unwrap(), Action::Alert)
200 .trigger
201 .cached_value
202 .clone()
203 .into_inner()
204 .unwrap(),
205 MetricValue::Bool($eval)
206 );
207 }};
208 }
209
210 #[fuchsia::test]
211 fn actions_collected_correctly() {
212 let metrics = make_metrics!({
213 "file":{
214 eval: {
215 "true": "0 == 0",
216 "false": "0 == 1",
217 "true_array": "[0 == 0]",
218 "false_array": "[0 == 1]"
219 }
220 }
221 });
222 let mut actions = Actions::new();
223 let mut action_file = ActionsSchema::new();
224 action_file.insert(
225 "do_true".to_string(),
226 Action::Alert(Alert {
227 trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
228 print: "True was fired".to_string(),
229 file_bug: Some("Some>Monorail>Component".to_string()),
230 tag: None,
231 severity: Severity::Warning,
232 }),
233 );
234 action_file.insert(
235 "do_false".to_string(),
236 Action::Alert(Alert {
237 trigger: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(),
238 print: "False was fired".to_string(),
239 file_bug: None,
240 tag: None,
241 severity: Severity::Warning,
242 }),
243 );
244 action_file.insert(
245 "do_true_array".to_string(),
246 Action::Alert(Alert {
247 trigger: ValueSource::try_from_expression_with_namespace("true_array", "file")
248 .unwrap(),
249 print: "True array was fired".to_string(),
250 file_bug: None,
251 tag: None,
252 severity: Severity::Warning,
253 }),
254 );
255 action_file.insert(
256 "do_false_array".to_string(),
257 Action::Alert(Alert {
258 trigger: ValueSource::try_from_expression_with_namespace("false_array", "file")
259 .unwrap(),
260 print: "False array was fired".to_string(),
261 file_bug: None,
262 tag: None,
263 severity: Severity::Warning,
264 }),
265 );
266
267 action_file.insert(
268 "do_operation".to_string(),
269 Action::Alert(Alert {
270 trigger: ValueSource::try_from_expression_with_namespace("0 < 10", "file").unwrap(),
271 print: "Inequality triggered".to_string(),
272 file_bug: None,
273 tag: None,
274 severity: Severity::Warning,
275 }),
276 );
277 actions.insert("file".to_string(), action_file);
278 let no_data = Vec::new();
279 let mut context = StructuredActionContext::new(&metrics, &actions, &no_data, None);
280 let results = context.process();
281 assert_eq!(
282 cast!(results.actions.get("file").unwrap().get("do_true").unwrap(), Action::Alert)
283 .file_bug
284 .as_ref()
285 .unwrap(),
286 "Some>Monorail>Component"
287 );
288 assert_eq!(
289 cast!(results.actions.get("file").unwrap().get("do_true").unwrap(), Action::Alert)
290 .print,
291 "True was fired"
292 );
293 trigger_eq!(results, "file", "do_true", true);
294 assert_eq!(
295 cast!(results.actions.get("file").unwrap().get("do_false").unwrap(), Action::Alert)
296 .print,
297 "False was fired"
298 );
299 trigger_eq!(results, "file", "do_false", false);
300 assert_eq!(
301 cast!(
302 results.actions.get("file").unwrap().get("do_true_array").unwrap(),
303 Action::Alert
304 )
305 .print,
306 "True array was fired"
307 );
308 trigger_eq!(results, "file", "do_true_array", true);
309 assert_eq!(
310 cast!(
311 results.actions.get("file").unwrap().get("do_false_array").unwrap(),
312 Action::Alert
313 )
314 .print,
315 "False array was fired"
316 );
317 trigger_eq!(results, "file", "do_false_array", false);
318 assert_eq!(
319 cast!(results.actions.get("file").unwrap().get("do_operation").unwrap(), Action::Alert)
320 .print,
321 "Inequality triggered"
322 );
323 trigger_eq!(results, "file", "do_operation", true);
324 }
325
326 #[fuchsia::test]
327 fn gauges_collected_correctly() {
328 let metrics = make_metrics!({
329 "file":{
330 eval: {
331 "gauge_f1": "2 / 5",
332 "gauge_f2": "4 / 5",
333 "gauge_f3": "6 / 5",
334 "gauge_i4": "9 // 2",
335 "gauge_i5": "11 // 2",
336 "gauge_i6": "13 // 2",
337 "gauge_b7": "2 == 2",
338 "gauge_b8": "2 > 2",
339 "gauge_s9": "'foo'"
340 }
341 }
342 });
343 let mut actions = Actions::new();
344 let mut action_file = ActionsSchema::new();
345 macro_rules! insert_gauge {
346 ($name:expr, $format:expr) => {
347 action_file.insert(
348 $name.to_string(),
349 Action::Gauge(Gauge {
350 value: ValueSource::try_from_expression_with_namespace($name, "file")
351 .unwrap(),
352 format: $format,
353 tag: None,
354 }),
355 );
356 };
357 }
358 insert_gauge!("gauge_f1", None);
359 insert_gauge!("gauge_f2", Some("percentage".to_string()));
360 insert_gauge!("gauge_f3", Some("unknown".to_string()));
361 insert_gauge!("gauge_i4", None);
362 insert_gauge!("gauge_i5", Some("percentage".to_string()));
363 insert_gauge!("gauge_i6", Some("unknown".to_string()));
364 insert_gauge!("gauge_b7", None);
365 insert_gauge!("gauge_b8", None);
366 insert_gauge!("gauge_s9", None);
367 actions.insert("file".to_string(), action_file);
368 let no_data = Vec::new();
369 let mut context = StructuredActionContext::new(&metrics, &actions, &no_data, None);
370
371 let results = context.process();
372
373 assert_eq!(
374 cast!(results.actions.get("file").unwrap().get("gauge_f1").unwrap(), Action::Gauge)
375 .value
376 .cached_value
377 .borrow()
378 .as_ref()
379 .unwrap()
380 .clone(),
381 MetricValue::Float(0.4)
382 );
383 assert_eq!(
384 cast!(results.actions.get("file").unwrap().get("gauge_f1").unwrap(), Action::Gauge)
385 .format,
386 None
387 );
388 assert_eq!(
389 cast!(results.actions.get("file").unwrap().get("gauge_f2").unwrap(), Action::Gauge)
390 .value
391 .cached_value
392 .borrow()
393 .as_ref()
394 .unwrap()
395 .clone(),
396 MetricValue::Float(0.8)
397 );
398 assert_eq!(
399 cast!(results.actions.get("file").unwrap().get("gauge_f2").unwrap(), Action::Gauge)
400 .format
401 .as_ref()
402 .unwrap(),
403 "percentage"
404 );
405 assert_eq!(
406 cast!(results.actions.get("file").unwrap().get("gauge_f3").unwrap(), Action::Gauge)
407 .value
408 .cached_value
409 .borrow()
410 .as_ref()
411 .unwrap()
412 .clone(),
413 MetricValue::Float(1.2)
414 );
415 assert_eq!(
416 cast!(results.actions.get("file").unwrap().get("gauge_f3").unwrap(), Action::Gauge)
417 .format
418 .as_ref()
419 .unwrap(),
420 "unknown"
421 );
422 assert_eq!(
423 cast!(results.actions.get("file").unwrap().get("gauge_i4").unwrap(), Action::Gauge)
424 .value
425 .cached_value
426 .borrow()
427 .as_ref()
428 .unwrap()
429 .clone(),
430 MetricValue::Int(4)
431 );
432 assert_eq!(
433 cast!(results.actions.get("file").unwrap().get("gauge_i4").unwrap(), Action::Gauge)
434 .format,
435 None
436 );
437 assert_eq!(
438 cast!(results.actions.get("file").unwrap().get("gauge_i5").unwrap(), Action::Gauge)
439 .value
440 .cached_value
441 .borrow()
442 .as_ref()
443 .unwrap()
444 .clone(),
445 MetricValue::Int(5)
446 );
447 assert_eq!(
448 cast!(results.actions.get("file").unwrap().get("gauge_i5").unwrap(), Action::Gauge)
449 .format
450 .as_ref()
451 .unwrap(),
452 "percentage"
453 );
454 assert_eq!(
455 cast!(results.actions.get("file").unwrap().get("gauge_i6").unwrap(), Action::Gauge)
456 .value
457 .cached_value
458 .borrow()
459 .as_ref()
460 .unwrap()
461 .clone(),
462 MetricValue::Int(6)
463 );
464 assert_eq!(
465 cast!(results.actions.get("file").unwrap().get("gauge_i6").unwrap(), Action::Gauge)
466 .format
467 .as_ref()
468 .unwrap(),
469 "unknown"
470 );
471 assert_eq!(
472 cast!(results.actions.get("file").unwrap().get("gauge_b7").unwrap(), Action::Gauge)
473 .value
474 .cached_value
475 .borrow()
476 .as_ref()
477 .unwrap()
478 .clone(),
479 MetricValue::Bool(true)
480 );
481 assert_eq!(
482 cast!(results.actions.get("file").unwrap().get("gauge_b7").unwrap(), Action::Gauge)
483 .format,
484 None
485 );
486 assert_eq!(
487 cast!(results.actions.get("file").unwrap().get("gauge_b8").unwrap(), Action::Gauge)
488 .value
489 .cached_value
490 .borrow()
491 .as_ref()
492 .unwrap()
493 .clone(),
494 MetricValue::Bool(false)
495 );
496 assert_eq!(
497 cast!(results.actions.get("file").unwrap().get("gauge_b8").unwrap(), Action::Gauge)
498 .format,
499 None
500 );
501 assert_eq!(
502 cast!(results.actions.get("file").unwrap().get("gauge_s9").unwrap(), Action::Gauge)
503 .value
504 .cached_value
505 .borrow()
506 .as_ref()
507 .unwrap()
508 .clone(),
509 MetricValue::String("foo".to_string())
510 );
511 assert_eq!(
512 cast!(results.actions.get("file").unwrap().get("gauge_s9").unwrap(), Action::Gauge)
513 .format,
514 None
515 );
516 }
517
518 #[fuchsia::test]
521 fn actions_cache_correctly() {
522 let metrics = make_metrics!({
523 "file":{
524 eval: {
525 "true": "0 == 0",
526 "false": "0 == 1",
527 "five": "5"
528 }
529 }
530 });
531 let mut actions = Actions::new();
532 let mut action_file = ActionsSchema::new();
533 action_file.insert(
534 "true_warning".to_string(),
535 Action::Alert(Alert {
536 trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
537 print: "True was fired".to_string(),
538 file_bug: None,
539 tag: None,
540 severity: Severity::Warning,
541 }),
542 );
543 action_file.insert(
544 "false_gauge".to_string(),
545 Action::Gauge(Gauge {
546 value: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(),
547 format: None,
548 tag: None,
549 }),
550 );
551 action_file.insert(
552 "true_snapshot".to_string(),
553 Action::Snapshot(Snapshot {
554 trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
555 repeat: ValueSource {
556 metric: Metric::Eval(
557 ExpressionContext::try_from_expression_with_namespace("five", "file")
558 .unwrap(),
559 ),
560 cached_value: RefCell::new(Some(MetricValue::Int(5))),
561 },
562 signature: "signature".to_string(),
563 }),
564 );
565 action_file.insert(
566 "test_snapshot".to_string(),
567 Action::Snapshot(Snapshot {
568 trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(),
569 repeat: ValueSource::try_from_expression_with_namespace("five", "file").unwrap(),
570 signature: "signature".to_string(),
571 }),
572 );
573 actions.insert("file".to_string(), action_file);
574 let no_data = Vec::new();
575 let mut context = StructuredActionContext::new(&metrics, &actions, &no_data, None);
576 context.process();
577
578 if let Action::Alert(alert) = actions.get("file").unwrap().get("true_warning").unwrap() {
580 assert_eq!(*alert.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
581 } else {
582 unreachable!("'true_warning' must be an Action::Alert")
583 }
584
585 if let Action::Gauge(gauge) = actions.get("file").unwrap().get("false_gauge").unwrap() {
587 assert_eq!(*gauge.value.cached_value.borrow(), Some(MetricValue::Bool(false)));
588 } else {
589 unreachable!("'false_gauge' must be an Action::Gauge")
590 }
591
592 if let Action::Snapshot(snapshot) =
594 actions.get("file").unwrap().get("true_snapshot").unwrap()
595 {
596 assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
597 assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5)));
598 } else {
599 unreachable!("'true_snapshot' must be an Action::Snapshot")
600 }
601
602 if let Action::Snapshot(snapshot) =
606 actions.get("file").unwrap().get("test_snapshot").unwrap()
607 {
608 assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true)));
609 assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5)));
610 } else {
611 unreachable!("'true_snapshot' must be an Action::Snapshot")
612 }
613 }
614}