blob: ad4732f5fafb779252ad8a7168e564f95651d9f5 [file] [log] [blame]
// Copyright 2019 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 log::error;
use {
super::{
config::DiagnosticData,
metrics::{Fetcher, FileDataFetcher, Metric, MetricState, MetricValue, Metrics},
plugins::{register_plugins, Plugin},
},
injectable_time::UtcTime,
lazy_static::lazy_static,
serde::{self, Deserialize},
std::collections::HashMap,
};
/// Provides the [metric_state] context to evaluate [Action]s and results of the [actions].
pub struct ActionContext<'a> {
actions: &'a Actions,
metric_state: MetricState<'a>,
action_results: ActionResults,
plugins: Vec<Box<dyn Plugin>>,
}
lazy_static! {
pub static ref REAL_CLOCK: UtcTime = UtcTime::new();
}
impl<'a> ActionContext<'a> {
pub fn new(
metrics: &'a Metrics,
actions: &'a Actions,
diagnostic_data: &'a Vec<DiagnosticData>,
) -> ActionContext<'a> {
let fetcher = FileDataFetcher::new(diagnostic_data);
let mut action_results = ActionResults::new();
fetcher.errors().iter().for_each(|e| {
action_results.add_warning(format!("[ERROR] {}", e));
});
ActionContext {
actions,
metric_state: MetricState::new(metrics, Fetcher::FileData(fetcher), &*REAL_CLOCK),
action_results,
plugins: register_plugins(),
}
}
}
/// Stores the results of each [Action] specified in [source] and
/// the [warnings] and [gauges] that are generated.
#[derive(Clone)]
pub struct ActionResults {
results: HashMap<String, bool>,
warnings: Vec<String>,
gauges: Vec<String>,
snapshots: Vec<SnapshotTrigger>,
sort_gauges: bool,
sub_results: Vec<(String, Box<ActionResults>)>,
}
impl ActionResults {
pub fn new() -> ActionResults {
ActionResults {
results: HashMap::new(),
warnings: Vec::new(),
gauges: Vec::new(),
snapshots: Vec::new(),
sort_gauges: true,
sub_results: Vec::new(),
}
}
pub fn set_result(&mut self, action: &str, value: bool) {
self.results.insert(action.to_string(), value);
}
pub fn add_warning(&mut self, warning: String) {
self.warnings.push(warning);
}
pub fn add_gauge(&mut self, gauge: String) {
self.gauges.push(gauge);
}
pub fn add_snapshot(&mut self, snapshot: SnapshotTrigger) {
self.snapshots.push(snapshot);
}
pub fn set_sort_gauges(&mut self, v: bool) {
self.sort_gauges = v;
}
pub fn sort_gauges(&self) -> bool {
self.sort_gauges
}
pub fn get_warnings(&self) -> &Vec<String> {
&self.warnings
}
pub fn get_gauges(&self) -> &Vec<String> {
&self.gauges
}
pub fn get_sub_results_mut(&mut self) -> &mut Vec<(String, Box<ActionResults>)> {
&mut self.sub_results
}
pub fn get_sub_results(&self) -> &Vec<(String, Box<ActionResults>)> {
&self.sub_results
}
}
/// [SnapshotTrigger] is the information needed to generate a request for a crash report.
/// It can be returned from the library as part of ActionResults.
#[derive(Debug, Clone, PartialEq)]
pub struct SnapshotTrigger {
pub interval: i64, // zx::Duration but this library has to run on host.
pub signature: String,
}
/// [Actions] are stored as a map of maps, both with string keys. The outer key
/// is the namespace for the inner key, which is the name of the [Action].
pub type Actions = HashMap<String, ActionsSchema>;
/// [ActionsSchema] stores the [Action]s from a single config file / namespace.
///
/// This struct is used to deserialize the [Action]s from the JSON-formatted
/// config file.
pub type ActionsSchema = HashMap<String, Action>;
/// Action represent actions that can be taken using an evaluated value(s).
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "type")]
pub enum Action {
Warning(Warning),
Gauge(Gauge),
Snapshot(Snapshot),
}
/// Action that is triggered if a predicate is met.
#[derive(Clone, Debug, Deserialize)]
pub struct Warning {
pub trigger: Metric, // An expression to evaluate which determines if this action triggers.
pub print: String, // What to print if trigger is true
pub tag: Option<String>, // An optional tag to associate with this Action
}
/// Action that displays percentage of value.
#[derive(Clone, Debug, Deserialize)]
pub struct Gauge {
pub value: Metric, // Value to surface
pub format: Option<String>, // Opaque type that determines how value should be formatted (e.g. percentage)
pub tag: Option<String>, // An optional tag to associate with this Action
}
/// Action that displays percentage of value.
#[derive(Clone, Debug, Deserialize)]
pub struct Snapshot {
pub trigger: Metric, // Take snapshot when this is true
pub repeat: Metric, // Expression evaluating to time delay before repeated triggers
pub signature: String, // Sent in the crash report
// There's no tag option because snapshot conditions are always news worth seeing.
}
impl Gauge {
pub fn get_formatted_value(&self, metric_value: MetricValue) -> String {
match metric_value {
MetricValue::Float(value) => match &self.format {
Some(format) if format.as_str() == "percentage" => {
format!("{:.2}%", value * 100.0f64)
}
_ => format!("{}", value),
},
MetricValue::Int(value) => match &self.format {
Some(format) if format.as_str() == "percentage" => format!("{}%", value * 100),
_ => format!("{}", value),
},
value => format!("{:?}", value),
}
}
}
impl Action {
pub fn get_tag(&self) -> Option<String> {
match self {
Action::Warning(action) => action.tag.clone(),
Action::Gauge(action) => action.tag.clone(),
Action::Snapshot(_) => None,
}
}
}
impl ActionContext<'_> {
/// Processes all actions, acting on the ones that trigger.
pub fn process(&mut self) -> &ActionResults {
if let Fetcher::FileData(file_data) = &self.metric_state.fetcher {
for plugin in &self.plugins {
self.action_results
.sub_results
.push((plugin.display_name().to_string(), Box::new(plugin.run(file_data))));
}
}
for (namespace, actions) in self.actions.iter() {
for (name, action) in actions.iter() {
match action {
Action::Warning(warning) => self.update_warnings(warning, namespace, name),
Action::Gauge(gauge) => self.update_gauges(gauge, namespace, name),
Action::Snapshot(snapshot) => self.update_snapshots(snapshot, namespace, name),
};
}
}
&self.action_results
}
/// Evaluate and return snapshots. Consume self.
pub fn into_snapshots(mut self) -> Vec<SnapshotTrigger> {
for (namespace, actions) in self.actions.iter() {
for (name, action) in actions.iter() {
if let Action::Snapshot(snapshot) = action {
self.update_snapshots(snapshot, namespace, name)
}
}
}
self.action_results.snapshots
}
/// Update warnings if condition is met.
fn update_warnings(&mut self, action: &Warning, namespace: &String, name: &String) {
let was_triggered = match self.metric_state.eval_action_metric(namespace, &action.trigger) {
MetricValue::Bool(true) => {
self.action_results.add_warning(format!("[WARNING] {}.", action.print));
true
}
MetricValue::Bool(false) => false,
MetricValue::Missing(reason) => {
self.action_results
.add_warning(format!("[MISSING] In config '{}': {}", namespace, reason));
false
}
other => {
self.action_results.add_warning(format!(
"[ERROR] Unexpected value type in config '{}' (need boolean): {}",
namespace, other
));
false
}
};
self.action_results.set_result(&format!("{}::{}", namespace, name), was_triggered);
}
/// Update snapshots if condition is met.
fn update_snapshots(&mut self, action: &Snapshot, namespace: &str, name: &str) {
let was_triggered = match self.metric_state.eval_action_metric(namespace, &action.trigger) {
MetricValue::Bool(true) => {
let interval = self.metric_state.eval_action_metric(namespace, &action.repeat);
match interval {
MetricValue::Int(interval) => {
let signature = action.signature.clone();
let output = SnapshotTrigger { interval, signature };
self.action_results.add_snapshot(output);
true
}
_ => {
self.action_results.add_warning(format!(
"Bad interval in config '{}': {:?}",
namespace, interval
));
#[cfg(target_os = "fuchsia")]
error!("Bad interval in config '{}': {:?}", namespace, interval);
false
}
}
}
MetricValue::Bool(false) => false,
MetricValue::Missing(reason) => {
#[cfg(target_os = "fuchsia")]
error!("Snapshot trigger was missing: {}", reason);
self.action_results
.add_warning(format!("[MISSING] In config '{}': {}", namespace, reason));
false
}
other => {
#[cfg(target_os = "fuchsia")]
error!(
"[ERROR] Unexpected value type in config '{}' (need boolean): {}",
namespace, other
);
self.action_results.add_warning(format!(
"[ERROR] Unexpected value type in config '{}' (need boolean): {}",
namespace, other
));
false
}
};
self.action_results.set_result(&format!("{}::{}", namespace, name), was_triggered);
}
/// Update gauges.
fn update_gauges(&mut self, action: &Gauge, namespace: &String, name: &String) {
let value = self.metric_state.eval_action_metric(namespace, &action.value);
self.action_results.add_gauge(format!("{}: {}", name, action.get_formatted_value(value)));
}
}
#[cfg(test)]
mod test {
use {
super::*,
crate::config::Source,
crate::metrics::{fetch::SelectorString, Metric, Metrics},
anyhow::Error,
std::convert::TryFrom,
};
/// Tells whether any of the stored values include a substring.
fn includes(values: &Vec<String>, substring: &str) -> bool {
for value in values {
if value.contains(substring) {
return true;
}
}
return false;
}
#[test]
fn actions_fire_correctly() {
let mut eval_file = HashMap::new();
eval_file.insert("true".to_string(), Metric::Eval("0==0".to_string()));
eval_file.insert("false".to_string(), Metric::Eval("0==1".to_string()));
eval_file.insert("true_array".to_string(), Metric::Eval("[0==0]".to_string()));
eval_file.insert("false_array".to_string(), Metric::Eval("[0==1]".to_string()));
let mut metrics = Metrics::new();
metrics.insert("file".to_string(), eval_file);
let mut actions = Actions::new();
let mut action_file = ActionsSchema::new();
action_file.insert(
"do_true".to_string(),
Action::Warning(Warning {
trigger: Metric::Eval("true".to_string()),
print: "True was fired".to_string(),
tag: None,
}),
);
action_file.insert(
"do_false".to_string(),
Action::Warning(Warning {
trigger: Metric::Eval("false".to_string()),
print: "False was fired".to_string(),
tag: None,
}),
);
action_file.insert(
"do_true_array".to_string(),
Action::Warning(Warning {
trigger: Metric::Eval("true_array".to_string()),
print: "True array was fired".to_string(),
tag: None,
}),
);
action_file.insert(
"do_false_array".to_string(),
Action::Warning(Warning {
trigger: Metric::Eval("false_array".to_string()),
print: "False array was fired".to_string(),
tag: None,
}),
);
action_file.insert(
"do_operation".to_string(),
Action::Warning(Warning {
trigger: Metric::Eval("0 < 10".to_string()),
print: "Inequality triggered".to_string(),
tag: None,
}),
);
actions.insert("file".to_string(), action_file);
let no_data = Vec::new();
let mut context = ActionContext::new(&metrics, &actions, &no_data);
let results = context.process();
assert!(includes(results.get_warnings(), "[WARNING] True was fired"));
assert!(includes(results.get_warnings(), "[WARNING] Inequality triggered"));
assert!(includes(results.get_warnings(), "[WARNING] True array was fired"));
assert!(!includes(results.get_warnings(), "False was fired"));
assert!(!includes(results.get_warnings(), "False array was fired"));
}
#[test]
fn gauges_fire_correctly() {
let mut eval_file = HashMap::new();
eval_file.insert("gauge_f1".to_string(), Metric::Eval("2 / 5".to_string()));
eval_file.insert("gauge_f2".to_string(), Metric::Eval("4 / 5".to_string()));
eval_file.insert("gauge_f3".to_string(), Metric::Eval("6 / 5".to_string()));
eval_file.insert("gauge_i4".to_string(), Metric::Eval("9 // 2".to_string()));
eval_file.insert("gauge_i5".to_string(), Metric::Eval("11 // 2".to_string()));
eval_file.insert("gauge_i6".to_string(), Metric::Eval("13 // 2".to_string()));
eval_file.insert("gauge_b7".to_string(), Metric::Eval("2 == 2".to_string()));
eval_file.insert("gauge_b8".to_string(), Metric::Eval("2 > 2".to_string()));
eval_file.insert("gauge_s9".to_string(), Metric::Eval("'foo'".to_string()));
let mut metrics = Metrics::new();
metrics.insert("file".to_string(), eval_file);
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: Metric::Eval($name.to_string()),
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 = ActionContext::new(&metrics, &actions, &no_data);
let results = context.process();
assert!(includes(results.get_gauges(), "gauge_f1: 0.4"));
assert!(includes(results.get_gauges(), "gauge_f2: 80.00%"));
assert!(includes(results.get_gauges(), "gauge_f3: 1.2"));
assert!(includes(results.get_gauges(), "gauge_i4: 4"));
assert!(includes(results.get_gauges(), "gauge_i5: 500%"));
assert!(includes(results.get_gauges(), "gauge_i6: 6"));
assert!(includes(results.get_gauges(), "gauge_b7: Bool(true)"));
assert!(includes(results.get_gauges(), "gauge_b8: Bool(false)"));
assert!(includes(results.get_gauges(), "gauge_s9: String(\"foo\")"));
}
#[test]
fn action_context_errors() {
let metrics = Metrics::new();
let actions = Actions::new();
let data = vec![DiagnosticData::new(
"inspect.json".to_string(),
Source::Inspect,
r#"
[
{
"moniker": "abcd",
"payload": {"root": {"val": 10}}
},
{
"moniker": "abcd2",
"payload": null
}
]
"#
.to_string(),
)
.expect("create data")];
let action_context = ActionContext::new(&metrics, &actions, &data);
assert_eq!(
&vec!["[ERROR] Unable to deserialize Inspect contents for abcd2 to node hierarchy"
.to_string()],
action_context.action_results.get_warnings()
);
}
#[test]
fn snapshots_update_correctly() -> Result<(), Error> {
let metrics = Metrics::new();
let actions = Actions::new();
let data = vec![];
let mut action_context = ActionContext::new(&metrics, &actions, &data);
let selector =
Metric::Selector(SelectorString::try_from("INSPECT:foo:bar:baz".to_string())?);
let true_value = Metric::Eval("1==1".to_string());
let false_value = Metric::Eval("1==2".to_string());
let five_value = Metric::Eval("5".to_string());
let foo_value = Metric::Eval("'foo'".to_string());
let missing_value = Metric::Eval("foo".to_string());
let snapshot_5_sig = SnapshotTrigger { interval: 5, signature: "signature".to_string() };
// Tester re-uses the same action_context, so results will accumulate.
macro_rules! tester {
($trigger:expr, $repeat:expr, $func:expr) => {
let selector_interval_action = Snapshot {
trigger: $trigger.clone(),
repeat: $repeat.clone(),
signature: "signature".to_string(),
};
action_context.update_snapshots(&selector_interval_action, "", "");
assert!($func(&action_context.action_results.snapshots));
};
}
type VT = Vec<SnapshotTrigger>;
// Verify it doesn't crash on bad inputs
tester!(true_value, selector, |s: &VT| s.is_empty());
tester!(true_value, foo_value, |s: &VT| s.is_empty());
tester!(true_value, missing_value, |s: &VT| s.is_empty());
tester!(selector, five_value, |s: &VT| s.is_empty());
tester!(foo_value, five_value, |s: &VT| s.is_empty());
tester!(five_value, five_value, |s: &VT| s.is_empty());
tester!(missing_value, five_value, |s: &VT| s.is_empty());
assert_eq!(action_context.action_results.warnings.len(), 7);
// False trigger shouldn't add a result
tester!(false_value, five_value, |s: &VT| s.is_empty());
tester!(true_value, five_value, |s| s == &vec![snapshot_5_sig.clone()]);
// We can have more than one of the same trigger in the results.
tester!(true_value, five_value, |s| s
== &vec![snapshot_5_sig.clone(), snapshot_5_sig.clone()]);
assert_eq!(action_context.action_results.warnings.len(), 7);
Ok(())
}
}