| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #[cfg(target_os = "fuchsia")] |
| use tracing::{error, warn}; |
| |
| use { |
| super::{ |
| config::DiagnosticData, |
| metrics::{ |
| fetch::{Fetcher, FileDataFetcher}, |
| metric_value::{MetricValue, Problem}, |
| MetricState, Metrics, ValueSource, |
| }, |
| plugins::{register_plugins, Plugin}, |
| }, |
| crate::act::{Action, Actions, Alert, Gauge, Snapshot}, |
| serde::Serialize, |
| std::collections::HashMap, |
| }; |
| |
| /// Provides the [metric_state] context to evaluate [Action]s and results of the [actions]. |
| #[derive(Clone, Debug, Serialize)] |
| pub struct TriageOutput { |
| actions: Actions, |
| metrics: Metrics, |
| plugin_results: HashMap<String, Vec<Action>>, |
| triage_errors: Vec<String>, |
| } |
| |
| impl TriageOutput { |
| /// Instantiates a [TriageOutput] populated with Maps corresponding to the provided namespaces. |
| pub fn new(namespaces: Vec<String>) -> TriageOutput { |
| let mut actions: Actions = HashMap::new(); |
| let mut metrics: HashMap<String, HashMap<String, ValueSource>> = HashMap::new(); |
| |
| // Avoid overhead of checking/creating new map when creating a synthetic action |
| // Namespaces with no triggered actions are empty |
| for namespace in namespaces { |
| metrics.insert(namespace.clone(), HashMap::new()); |
| actions.insert(namespace, HashMap::new()); |
| } |
| |
| TriageOutput { actions, metrics, triage_errors: Vec::new(), plugin_results: HashMap::new() } |
| } |
| |
| /// Add an error generated while creating TriageOutput |
| pub fn add_error(&mut self, error: String) { |
| self.triage_errors.push(error); |
| } |
| |
| /// Add an Action to processing during TriageOutput |
| pub fn add_action(&mut self, namespace: String, name: String, action: Action) { |
| if let Some(actions_map) = self.actions.get_mut(&namespace) { |
| actions_map.insert(name, action); |
| } |
| } |
| |
| /// Returns true if any triggered [Warning]s or [Error]s are generated while building the |
| /// [TriageOutput], and false otherwise. |
| pub fn has_reportable_issues(&self) -> bool { |
| for (_namespace, actions_map) in &self.actions { |
| for (_name, action) in actions_map { |
| if action.has_reportable_issue() { |
| return true; |
| } |
| } |
| } |
| false |
| } |
| } |
| |
| pub struct StructuredActionContext<'a> { |
| actions: &'a Actions, |
| metric_state: MetricState<'a>, |
| triage_output: TriageOutput, |
| plugins: Vec<Box<dyn Plugin>>, |
| } |
| |
| impl<'a> StructuredActionContext<'a> { |
| pub(crate) fn new( |
| metrics: &'a Metrics, |
| actions: &'a Actions, |
| diagnostic_data: &'a Vec<DiagnosticData>, |
| now: Option<i64>, |
| ) -> StructuredActionContext<'a> { |
| let fetcher = FileDataFetcher::new(diagnostic_data); |
| let mut triage_output = TriageOutput::new(metrics.keys().cloned().collect::<Vec<String>>()); |
| fetcher.errors().iter().for_each(|e| { |
| triage_output.add_error(format!("[DEBUG: BAD DATA] {}", e)); |
| }); |
| |
| StructuredActionContext { |
| actions, |
| metric_state: MetricState::new(metrics, Fetcher::FileData(fetcher), now), |
| triage_output, |
| plugins: register_plugins(), |
| } |
| } |
| } |
| |
| impl StructuredActionContext<'_> { |
| // TODO(https://fxbug.dev/42178829): This must be refactored into `build`. |
| // remove the unnecessary code blocks after refactor. |
| /// Processes all actions, acting on the ones that trigger. |
| pub fn process(&mut self) -> &TriageOutput { |
| for (namespace, actions) in self.actions.iter() { |
| for (name, action) in actions.iter() { |
| match action { |
| Action::Alert(alert) => self.update_alerts(alert, namespace, name), |
| Action::Gauge(gauge) => self.update_gauges(gauge, namespace, name), |
| Action::Snapshot(snapshot) => self.update_snapshots(snapshot, namespace, name), |
| }; |
| } |
| } |
| |
| self.metric_state.evaluate_all_metrics(); |
| |
| self.triage_output.metrics = self.metric_state.metrics.clone(); |
| |
| if let Fetcher::FileData(file_data) = &self.metric_state.fetcher { |
| for plugin in &self.plugins { |
| let actions = plugin.run_structured(file_data); |
| self.triage_output.plugin_results.insert(plugin.name().to_string(), actions); |
| } |
| } |
| |
| &self.triage_output |
| } |
| |
| /// Update alerts after ensuring their trigger is evaluated. |
| fn update_alerts(&mut self, action: &Alert, namespace: &String, name: &String) { |
| self.metric_state.eval_action_metric(namespace, &action.trigger); |
| self.triage_output.add_action( |
| namespace.clone(), |
| name.clone(), |
| Action::Alert(action.clone()), |
| ); |
| } |
| |
| /// Populate snapshots. Log a warning if the condition evaluates to a reportable Problem. |
| fn update_snapshots(&mut self, action: &Snapshot, namespace: &str, name: &str) { |
| let snapshot_trigger = self.metric_state.eval_action_metric(namespace, &action.trigger); |
| self.metric_state.eval_action_metric(namespace, &action.repeat); |
| self.triage_output.add_action( |
| namespace.to_string(), |
| name.to_string(), |
| Action::Snapshot(action.clone()), |
| ); |
| match snapshot_trigger { |
| MetricValue::Bool(true) => {} |
| MetricValue::Bool(false) => {} |
| MetricValue::Problem(Problem::Ignore(_)) => {} |
| MetricValue::Problem(_reason) => { |
| #[cfg(target_os = "fuchsia")] |
| warn!( |
| "Snapshot trigger was not boolean in '{}::{}': {:?}", |
| namespace, name, _reason, |
| ); |
| } |
| _other => { |
| #[cfg(target_os = "fuchsia")] |
| error!( |
| "[DEBUG: BAD CONFIG] Unexpected value type in config '{}::{}' (need boolean): {}", |
| namespace, |
| name, |
| _other, |
| ); |
| } |
| }; |
| } |
| |
| /// Evaluate a [Gauge] and collect the evaluated action in the [TriageOutput]. |
| fn update_gauges(&mut self, action: &Gauge, namespace: &String, name: &String) { |
| // The metric value is cached and added to output |
| self.metric_state.eval_action_metric(namespace, &action.value); |
| self.triage_output.add_action( |
| namespace.to_string(), |
| name.to_string(), |
| Action::Gauge(action.clone()), |
| ) |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use { |
| super::*, |
| crate::{ |
| act::{ActionsSchema, Severity}, |
| make_metrics, |
| metrics::{ExpressionContext, Metric}, |
| }, |
| std::cell::RefCell, |
| }; |
| |
| macro_rules! cast { |
| ($target: expr, $pat: path) => {{ |
| if let $pat(a) = $target { |
| a |
| } else { |
| panic!("mismatch variant when cast to {}", stringify!($pat)); |
| } |
| }}; |
| } |
| |
| macro_rules! trigger_eq { |
| ($results: expr, $file: expr, $name: expr, $eval: expr) => {{ |
| assert_eq!( |
| cast!($results.actions.get($file).unwrap().get($name).unwrap(), Action::Alert) |
| .trigger |
| .cached_value |
| .clone() |
| .into_inner() |
| .unwrap(), |
| MetricValue::Bool($eval) |
| ); |
| }}; |
| } |
| |
| #[fuchsia::test] |
| fn actions_collected_correctly() { |
| let metrics = make_metrics!({ |
| "file":{ |
| eval: { |
| "true": "0 == 0", |
| "false": "0 == 1", |
| "true_array": "[0 == 0]", |
| "false_array": "[0 == 1]" |
| } |
| } |
| }); |
| let mut actions = Actions::new(); |
| let mut action_file = ActionsSchema::new(); |
| action_file.insert( |
| "do_true".to_string(), |
| Action::Alert(Alert { |
| trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(), |
| print: "True was fired".to_string(), |
| file_bug: Some("Some>Monorail>Component".to_string()), |
| tag: None, |
| severity: Severity::Warning, |
| }), |
| ); |
| action_file.insert( |
| "do_false".to_string(), |
| Action::Alert(Alert { |
| trigger: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(), |
| print: "False was fired".to_string(), |
| file_bug: None, |
| tag: None, |
| severity: Severity::Warning, |
| }), |
| ); |
| action_file.insert( |
| "do_true_array".to_string(), |
| Action::Alert(Alert { |
| trigger: ValueSource::try_from_expression_with_namespace("true_array", "file") |
| .unwrap(), |
| print: "True array was fired".to_string(), |
| file_bug: None, |
| tag: None, |
| severity: Severity::Warning, |
| }), |
| ); |
| action_file.insert( |
| "do_false_array".to_string(), |
| Action::Alert(Alert { |
| trigger: ValueSource::try_from_expression_with_namespace("false_array", "file") |
| .unwrap(), |
| print: "False array was fired".to_string(), |
| file_bug: None, |
| tag: None, |
| severity: Severity::Warning, |
| }), |
| ); |
| |
| action_file.insert( |
| "do_operation".to_string(), |
| Action::Alert(Alert { |
| trigger: ValueSource::try_from_expression_with_namespace("0 < 10", "file").unwrap(), |
| print: "Inequality triggered".to_string(), |
| file_bug: None, |
| tag: None, |
| severity: Severity::Warning, |
| }), |
| ); |
| actions.insert("file".to_string(), action_file); |
| let no_data = Vec::new(); |
| let mut context = StructuredActionContext::new(&metrics, &actions, &no_data, None); |
| let results = context.process(); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("do_true").unwrap(), Action::Alert) |
| .file_bug |
| .as_ref() |
| .unwrap(), |
| "Some>Monorail>Component" |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("do_true").unwrap(), Action::Alert) |
| .print, |
| "True was fired" |
| ); |
| trigger_eq!(results, "file", "do_true", true); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("do_false").unwrap(), Action::Alert) |
| .print, |
| "False was fired" |
| ); |
| trigger_eq!(results, "file", "do_false", false); |
| assert_eq!( |
| cast!( |
| results.actions.get("file").unwrap().get("do_true_array").unwrap(), |
| Action::Alert |
| ) |
| .print, |
| "True array was fired" |
| ); |
| trigger_eq!(results, "file", "do_true_array", true); |
| assert_eq!( |
| cast!( |
| results.actions.get("file").unwrap().get("do_false_array").unwrap(), |
| Action::Alert |
| ) |
| .print, |
| "False array was fired" |
| ); |
| trigger_eq!(results, "file", "do_false_array", false); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("do_operation").unwrap(), Action::Alert) |
| .print, |
| "Inequality triggered" |
| ); |
| trigger_eq!(results, "file", "do_operation", true); |
| } |
| |
| #[fuchsia::test] |
| fn gauges_collected_correctly() { |
| let metrics = make_metrics!({ |
| "file":{ |
| eval: { |
| "gauge_f1": "2 / 5", |
| "gauge_f2": "4 / 5", |
| "gauge_f3": "6 / 5", |
| "gauge_i4": "9 // 2", |
| "gauge_i5": "11 // 2", |
| "gauge_i6": "13 // 2", |
| "gauge_b7": "2 == 2", |
| "gauge_b8": "2 > 2", |
| "gauge_s9": "'foo'" |
| } |
| } |
| }); |
| let mut actions = Actions::new(); |
| let mut action_file = ActionsSchema::new(); |
| macro_rules! insert_gauge { |
| ($name:expr, $format:expr) => { |
| action_file.insert( |
| $name.to_string(), |
| Action::Gauge(Gauge { |
| value: ValueSource::try_from_expression_with_namespace($name, "file") |
| .unwrap(), |
| format: $format, |
| tag: None, |
| }), |
| ); |
| }; |
| } |
| insert_gauge!("gauge_f1", None); |
| insert_gauge!("gauge_f2", Some("percentage".to_string())); |
| insert_gauge!("gauge_f3", Some("unknown".to_string())); |
| insert_gauge!("gauge_i4", None); |
| insert_gauge!("gauge_i5", Some("percentage".to_string())); |
| insert_gauge!("gauge_i6", Some("unknown".to_string())); |
| insert_gauge!("gauge_b7", None); |
| insert_gauge!("gauge_b8", None); |
| insert_gauge!("gauge_s9", None); |
| actions.insert("file".to_string(), action_file); |
| let no_data = Vec::new(); |
| let mut context = StructuredActionContext::new(&metrics, &actions, &no_data, None); |
| |
| let results = context.process(); |
| |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_f1").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Float(0.4) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_f1").unwrap(), Action::Gauge) |
| .format, |
| None |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_f2").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Float(0.8) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_f2").unwrap(), Action::Gauge) |
| .format |
| .as_ref() |
| .unwrap(), |
| "percentage" |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_f3").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Float(1.2) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_f3").unwrap(), Action::Gauge) |
| .format |
| .as_ref() |
| .unwrap(), |
| "unknown" |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_i4").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Int(4) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_i4").unwrap(), Action::Gauge) |
| .format, |
| None |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_i5").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Int(5) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_i5").unwrap(), Action::Gauge) |
| .format |
| .as_ref() |
| .unwrap(), |
| "percentage" |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_i6").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Int(6) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_i6").unwrap(), Action::Gauge) |
| .format |
| .as_ref() |
| .unwrap(), |
| "unknown" |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_b7").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Bool(true) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_b7").unwrap(), Action::Gauge) |
| .format, |
| None |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_b8").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::Bool(false) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_b8").unwrap(), Action::Gauge) |
| .format, |
| None |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_s9").unwrap(), Action::Gauge) |
| .value |
| .cached_value |
| .borrow() |
| .as_ref() |
| .unwrap() |
| .clone(), |
| MetricValue::String("foo".to_string()) |
| ); |
| assert_eq!( |
| cast!(results.actions.get("file").unwrap().get("gauge_s9").unwrap(), Action::Gauge) |
| .format, |
| None |
| ); |
| } |
| |
| // TODO(https://fxbug.dev/42178827): Additional unit tests required. |
| |
| #[fuchsia::test] |
| fn actions_cache_correctly() { |
| let metrics = make_metrics!({ |
| "file":{ |
| eval: { |
| "true": "0 == 0", |
| "false": "0 == 1", |
| "five": "5" |
| } |
| } |
| }); |
| let mut actions = Actions::new(); |
| let mut action_file = ActionsSchema::new(); |
| action_file.insert( |
| "true_warning".to_string(), |
| Action::Alert(Alert { |
| trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(), |
| print: "True was fired".to_string(), |
| file_bug: None, |
| tag: None, |
| severity: Severity::Warning, |
| }), |
| ); |
| action_file.insert( |
| "false_gauge".to_string(), |
| Action::Gauge(Gauge { |
| value: ValueSource::try_from_expression_with_namespace("false", "file").unwrap(), |
| format: None, |
| tag: None, |
| }), |
| ); |
| action_file.insert( |
| "true_snapshot".to_string(), |
| Action::Snapshot(Snapshot { |
| trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(), |
| repeat: ValueSource { |
| metric: Metric::Eval( |
| ExpressionContext::try_from_expression_with_namespace("five", "file") |
| .unwrap(), |
| ), |
| cached_value: RefCell::new(Some(MetricValue::Int(5))), |
| }, |
| signature: "signature".to_string(), |
| }), |
| ); |
| action_file.insert( |
| "test_snapshot".to_string(), |
| Action::Snapshot(Snapshot { |
| trigger: ValueSource::try_from_expression_with_namespace("true", "file").unwrap(), |
| repeat: ValueSource::try_from_expression_with_namespace("five", "file").unwrap(), |
| signature: "signature".to_string(), |
| }), |
| ); |
| actions.insert("file".to_string(), action_file); |
| let no_data = Vec::new(); |
| let mut context = StructuredActionContext::new(&metrics, &actions, &no_data, None); |
| context.process(); |
| |
| // Ensure Alert caches correctly |
| if let Action::Alert(alert) = actions.get("file").unwrap().get("true_warning").unwrap() { |
| assert_eq!(*alert.trigger.cached_value.borrow(), Some(MetricValue::Bool(true))); |
| } else { |
| unreachable!("'true_warning' must be an Action::Alert") |
| } |
| |
| // Ensure Gauge caches correctly |
| if let Action::Gauge(gauge) = actions.get("file").unwrap().get("false_gauge").unwrap() { |
| assert_eq!(*gauge.value.cached_value.borrow(), Some(MetricValue::Bool(false))); |
| } else { |
| unreachable!("'false_gauge' must be an Action::Gauge") |
| } |
| |
| // Ensure Snapshot caches correctly |
| if let Action::Snapshot(snapshot) = |
| actions.get("file").unwrap().get("true_snapshot").unwrap() |
| { |
| assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true))); |
| assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5))); |
| } else { |
| unreachable!("'true_snapshot' must be an Action::Snapshot") |
| } |
| |
| // Ensure value-calculation does not fail for a Snapshot with an empty cache. |
| // The cached value for 'repeat' is expected to be pre-calculated during deserialization |
| // however, an empty cached value should still be supported. |
| if let Action::Snapshot(snapshot) = |
| actions.get("file").unwrap().get("test_snapshot").unwrap() |
| { |
| assert_eq!(*snapshot.trigger.cached_value.borrow(), Some(MetricValue::Bool(true))); |
| assert_eq!(*snapshot.repeat.cached_value.borrow(), Some(MetricValue::Int(5))); |
| } else { |
| unreachable!("'true_snapshot' must be an Action::Snapshot") |
| } |
| } |
| } |