fuchsia_triage/
act_structured.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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/// Provides the [metric_state] context to evaluate [Action]s and results of the [actions].
16#[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    /// Instantiates a [TriageOutput] populated with Maps corresponding to the provided namespaces.
26    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        // Avoid overhead of checking/creating new map when creating a synthetic action
31        // Namespaces with no triggered actions are empty
32        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    /// Add an error generated while creating TriageOutput
41    pub fn add_error(&mut self, error: String) {
42        self.triage_errors.push(error);
43    }
44
45    /// Add an Action to processing during TriageOutput
46    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    /// Returns true if any triggered [Warning]s or [Error]s are generated while building the
53    /// [TriageOutput], and false otherwise.
54    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    // TODO(https://fxbug.dev/42178829): This must be refactored into `build`.
97    // remove the unnecessary code blocks after refactor.
98    /// Processes all actions, acting on the ones that trigger.
99    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    /// Update alerts after ensuring their trigger is evaluated.
125    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    /// Populate snapshots. Log a warning if the condition evaluates to a reportable Problem.
135    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    /// Evaluate a [Gauge] and collect the evaluated action in the [TriageOutput].
167    fn update_gauges(&mut self, action: &Gauge, namespace: &String, name: &String) {
168        // The metric value is cached and added to output
169        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    // TODO(https://fxbug.dev/42178827): Additional unit tests required.
519
520    #[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        // Ensure Alert caches correctly
579        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        // Ensure Gauge caches correctly
586        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        // Ensure Snapshot caches correctly
593        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        // Ensure value-calculation does not fail for a Snapshot with an empty cache.
603        // The cached value for 'repeat' is expected to be pre-calculated during deserialization
604        // however, an empty cached value should still be supported.
605        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}