1use crate::act::{validate_action, Actions, ActionsSchema, Severity};
6use crate::metrics::fetch::{InspectFetcher, KeyValueFetcher, SelectorString, TextFetcher};
7use crate::metrics::{Metric, Metrics, ValueSource};
8use crate::validate::{validate, Trials, TrialsSchema};
9use crate::Action;
10use anyhow::{bail, format_err, Context, Error};
11use num_derive::FromPrimitive;
12use serde::{Deserialize, Deserializer};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq)]
19pub enum Source {
20 Inspect = 0,
21 Klog = 1,
22 Syslog = 2,
23 Bootlog = 3,
24 Annotations = 4,
25}
26
27#[derive(Deserialize, Default, Debug)]
30#[serde(deny_unknown_fields)]
31pub struct ConfigFileSchema {
32 #[serde(rename = "select")]
34 pub file_selectors: Option<HashMap<String, SelectorEntry>>,
35 #[serde(rename = "eval")]
37 pub file_evals: Option<HashMap<String, String>>,
38 #[serde(rename = "act")]
40 pub(crate) file_actions: Option<HashMap<String, ActionConfig>>,
41 #[serde(rename = "test")]
44 pub file_tests: Option<TrialsSchema>,
45}
46
47#[derive(Debug)]
51pub struct SelectorEntry(Vec<String>);
52
53impl<'de> Deserialize<'de> for SelectorEntry {
54 fn deserialize<D>(d: D) -> Result<Self, D::Error>
55 where
56 D: Deserializer<'de>,
57 {
58 struct SelectorVec(std::marker::PhantomData<Vec<String>>);
59
60 impl<'de> serde::de::Visitor<'de> for SelectorVec {
61 type Value = Vec<String>;
62
63 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.write_str("either a single selector or an array of selectors")
65 }
66
67 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
68 where
69 E: serde::de::Error,
70 {
71 Ok(vec![value.to_string()])
72 }
73
74 fn visit_seq<A>(self, mut value: A) -> Result<Self::Value, A::Error>
75 where
76 A: serde::de::SeqAccess<'de>,
77 {
78 let mut out = vec![];
79 while let Some(s) = value.next_element::<String>()? {
80 out.push(s);
81 }
82 if out.is_empty() {
83 use serde::de::Error;
84 Err(A::Error::invalid_length(0, &"expected at least one selector"))
85 } else {
86 Ok(out)
87 }
88 }
89 }
90
91 Ok(SelectorEntry(d.deserialize_any(SelectorVec(std::marker::PhantomData))?))
92 }
93}
94
95#[derive(Deserialize, Debug)]
97#[serde(tag = "type")]
98pub enum ActionConfig {
99 Alert {
100 trigger: String,
101 print: String,
102 file_bug: Option<String>,
103 tag: Option<String>,
104 severity: Severity,
105 },
106 Warning {
107 trigger: String,
108 print: String,
109 file_bug: Option<String>,
110 tag: Option<String>,
111 },
112 Gauge {
113 value: String,
114 format: Option<String>,
115 tag: Option<String>,
116 },
117 Snapshot {
118 trigger: String,
119 repeat: String,
120 signature: String,
121 },
122}
123
124impl ConfigFileSchema {
125 fn try_from_str_with_namespace(s: &str, namespace: &str) -> Result<Self, anyhow::Error> {
126 let schema = serde_json5::from_str::<ConfigFileSchema>(s)
127 .map_err(|e| format_err!("Unable to deserialize config file {}", e))?;
128 validate_config(&schema, namespace)?;
129 Ok(schema)
130 }
131}
132
133impl TryFrom<String> for ConfigFileSchema {
134 type Error = anyhow::Error;
135
136 fn try_from(s: String) -> Result<Self, Self::Error> {
137 ConfigFileSchema::try_from_str_with_namespace(&s, "")
138 }
139}
140
141fn validate_config(config: &ConfigFileSchema, namespace: &str) -> Result<(), Error> {
142 if let Some(ref actions_config) = config.file_actions {
143 for (action_name, action_config) in actions_config.iter() {
144 validate_action(action_name, action_config, namespace)?;
145 }
146 }
147 Ok(())
148}
149
150pub enum DataFetcher {
151 Inspect(InspectFetcher),
152 Text(TextFetcher),
153 KeyValue(KeyValueFetcher),
154 None,
155}
156
157pub struct DiagnosticData {
159 pub name: String,
160 pub source: Source,
161 pub data: DataFetcher,
162}
163
164impl DiagnosticData {
165 pub fn new(name: String, source: Source, contents: String) -> Result<DiagnosticData, Error> {
166 let data = match source {
167 Source::Inspect => DataFetcher::Inspect(
168 InspectFetcher::try_from(&*contents).context("Parsing inspect.json")?,
169 ),
170 Source::Syslog | Source::Klog | Source::Bootlog => {
171 DataFetcher::Text(TextFetcher::from(&*contents))
172 }
173 Source::Annotations => DataFetcher::KeyValue(
174 KeyValueFetcher::try_from(&*contents).context("Parsing annotations")?,
175 ),
176 };
177 Ok(DiagnosticData { name, source, data })
178 }
179
180 pub fn new_empty(name: String, source: Source) -> DiagnosticData {
181 DiagnosticData { name, source, data: DataFetcher::None }
182 }
183}
184
185pub struct ParseResult {
186 pub metrics: Metrics,
187 pub(crate) actions: Actions,
188 pub tests: Trials,
189}
190
191impl ParseResult {
192 pub fn new(
193 configs: &HashMap<String, String>,
194 action_tag_directive: &ActionTagDirective,
195 ) -> Result<ParseResult, Error> {
196 let mut actions = HashMap::new();
197 let mut metrics = HashMap::new();
198 let mut tests = HashMap::new();
199
200 for (namespace, file_data) in configs {
201 let file_config =
202 match ConfigFileSchema::try_from_str_with_namespace(file_data, namespace) {
203 Ok(c) => c,
204 Err(e) => bail!("Parsing file '{}': {}", namespace, e),
205 };
206 let ConfigFileSchema { file_actions, file_selectors, file_evals, file_tests } =
207 file_config;
208 let file_actions_config = file_actions.unwrap_or_else(HashMap::new);
210 let file_actions = file_actions_config
211 .into_iter()
212 .map(|(k, action_config)| {
213 Action::from_config_with_namespace(action_config, namespace)
214 .map(|action| (k, action))
215 })
216 .collect::<Result<HashMap<_, _>, _>>()?;
217 let file_selectors = file_selectors.unwrap_or_else(HashMap::new);
218 let file_evals = file_evals.unwrap_or_else(HashMap::new);
219 let file_tests = file_tests.unwrap_or_else(HashMap::new);
220 let file_actions = filter_actions(file_actions, action_tag_directive);
221 let mut file_metrics = HashMap::new();
222 for (key, value) in file_selectors.into_iter() {
223 let mut selectors = vec![];
224 for v in value.0 {
225 selectors.push(SelectorString::try_from(v)?);
226 }
227 file_metrics.insert(key, ValueSource::new(Metric::Selector(selectors)));
228 }
229 for (key, value) in file_evals.into_iter() {
230 if file_metrics.contains_key(&key) {
231 bail!("Duplicate metric name {} in file {}", key, namespace);
232 }
233 file_metrics.insert(
234 key,
235 ValueSource::try_from_expression_with_namespace(&value, namespace)?,
236 );
237 }
238 metrics.insert(namespace.clone(), file_metrics);
239 actions.insert(namespace.clone(), file_actions);
240 tests.insert(namespace.clone(), file_tests);
241 }
242
243 Ok(ParseResult { actions, metrics, tests })
244 }
245
246 pub fn all_selectors(&self) -> Vec<String> {
247 let mut result = Vec::new();
248 for (_, metric_set) in self.metrics.iter() {
249 for (_, value_source) in metric_set.iter() {
250 if let Metric::Selector(selectors) = &value_source.metric {
251 for selector in selectors {
252 result.push(selector.full_selector.to_owned());
253 }
254 }
255 }
256 }
257 result
258 }
259
260 pub fn validate(&self) -> Result<(), Error> {
261 validate(self)
262 }
263
264 pub fn reset_state(&self) {
265 for (_, metric_set) in self.metrics.iter() {
266 for (_, value_source) in metric_set.iter() {
267 *value_source.cached_value.borrow_mut() = None;
268 }
269 }
270
271 for (_, action_set) in self.actions.iter() {
272 for (_, action) in action_set.iter() {
273 match action {
274 Action::Alert(alert) => {
275 *alert.trigger.cached_value.borrow_mut() = None;
276 }
277 Action::Gauge(gauge) => {
278 *gauge.value.cached_value.borrow_mut() = None;
279 }
280 Action::Snapshot(snapshot) => {
281 *snapshot.trigger.cached_value.borrow_mut() = None;
282 *snapshot.repeat.cached_value.borrow_mut() = None;
283 }
284 }
285 }
286 }
287 }
288}
289
290pub enum ActionTagDirective {
292 AllowAll,
294
295 Include(Vec<String>),
297
298 Exclude(Vec<String>),
300}
301
302impl ActionTagDirective {
303 pub fn from_tags(tags: Vec<String>, exclude_tags: Vec<String>) -> ActionTagDirective {
312 match (tags.is_empty(), exclude_tags.is_empty()) {
313 (false, _) => ActionTagDirective::Include(tags),
315 (true, false) => ActionTagDirective::Exclude(exclude_tags),
317 _ => ActionTagDirective::AllowAll,
318 }
319 }
320}
321
322pub(crate) fn filter_actions(
330 actions: ActionsSchema,
331 action_directive: &ActionTagDirective,
332) -> ActionsSchema {
333 match action_directive {
334 ActionTagDirective::AllowAll => actions,
335 ActionTagDirective::Include(tags) => actions
336 .into_iter()
337 .filter(|(_, a)| match &a.get_tag() {
338 Some(tag) => tags.contains(tag),
339 None => false,
340 })
341 .collect(),
342 ActionTagDirective::Exclude(tags) => actions
343 .into_iter()
344 .filter(|(_, a)| match &a.get_tag() {
345 Some(tag) => !tags.contains(tag),
346 None => true,
347 })
348 .collect(),
349 }
350}
351
352#[cfg(test)]
353mod test {
354 use super::*;
355 use crate::act::Alert;
356 use fidl_fuchsia_feedback::MAX_CRASH_SIGNATURE_LENGTH;
357 use maplit::hashmap;
358
359 #[fuchsia::test]
363 fn inspect_data_from_works() -> Result<(), Error> {
364 assert!(InspectFetcher::try_from("foo").is_err(), "'foo' isn't valid JSON");
365 assert!(InspectFetcher::try_from(r#"{"a":5}"#).is_err(), "Needed an array");
366 assert!(InspectFetcher::try_from("[]").is_ok(), "A JSON array should have worked");
367 Ok(())
368 }
369
370 #[fuchsia::test]
371 fn action_tag_directive_from_tags_allow_all() {
372 let result = ActionTagDirective::from_tags(vec![], vec![]);
373 match result {
374 ActionTagDirective::AllowAll => (),
375 _ => panic!("failed to create correct ActionTagDirective"),
376 }
377 }
378
379 #[fuchsia::test]
380 fn action_tag_directive_from_tags_include() {
381 let result =
382 ActionTagDirective::from_tags(vec!["t1".to_string(), "t2".to_string()], vec![]);
383 match result {
384 ActionTagDirective::Include(tags) => {
385 assert_eq!(tags, vec!["t1".to_string(), "t2".to_string()])
386 }
387 _ => panic!("failed to create correct ActionTagDirective"),
388 }
389 }
390
391 #[fuchsia::test]
392 fn action_tag_directive_from_tags_include_override_exclude() {
393 let result = ActionTagDirective::from_tags(
394 vec!["t1".to_string(), "t2".to_string()],
395 vec!["t3".to_string()],
396 );
397 match result {
398 ActionTagDirective::Include(tags) => {
399 assert_eq!(tags, vec!["t1".to_string(), "t2".to_string()])
400 }
401 _ => panic!("failed to create correct ActionTagDirective"),
402 }
403 }
404
405 #[fuchsia::test]
406 fn action_tag_directive_from_tags_exclude() {
407 let result =
408 ActionTagDirective::from_tags(vec![], vec!["t1".to_string(), "t2".to_string()]);
409 match result {
410 ActionTagDirective::Exclude(tags) => {
411 assert_eq!(tags, vec!["t1".to_string(), "t2".to_string()])
412 }
413 _ => panic!("failed to create correct ActionTagDirective"),
414 }
415 }
416
417 macro_rules! actions_schema {
419 ( $($key:expr => $contents:expr, $tag:expr),+ ) => {
420 {
421 let mut m = ActionsSchema::new();
422 let trigger = ValueSource::try_from_expression_with_default_namespace("a_trigger").unwrap();
423
424 $(
425 let action = Action::Alert(Alert {
426 trigger: trigger.clone(),
427 print: $contents.to_string(),
428 tag: $tag,
429 file_bug: None,
430 severity: Severity::Warning,
431 });
432 m.insert($key.to_string(), action);
433 )+
434 m
435 }
436 }
437 }
438
439 macro_rules! assert_has_action {
441 ($result:expr, $key:expr, $contents:expr) => {
442 match $result.get(&$key.to_string()) {
443 Some(Action::Alert(a)) => {
444 assert_eq!(a.print, $contents.to_string());
445 }
446 _ => {
447 assert!(false);
448 }
449 }
450 };
451 }
452
453 #[fuchsia::test]
454 fn filter_actions_allow_all() {
455 let result = filter_actions(
456 actions_schema! {
457 "no_tag" => "foo", None,
458 "tagged" => "bar", Some("tag".to_string())
459 },
460 &ActionTagDirective::AllowAll,
461 );
462 assert_eq!(result.len(), 2);
463 }
464
465 #[fuchsia::test]
466 fn filter_actions_include_one_tag() {
467 let result = filter_actions(
468 actions_schema! {
469 "1" => "p1", Some("ignore".to_string()),
470 "2" => "p2", Some("tag".to_string()),
471 "3" => "p3", Some("tag".to_string())
472 },
473 &ActionTagDirective::Include(vec!["tag".to_string()]),
474 );
475 assert_eq!(result.len(), 2);
476 assert_has_action!(result, "2", "p2");
477 assert_has_action!(result, "3", "p3");
478 }
479
480 #[fuchsia::test]
481 fn filter_actions_include_many_tags() {
482 let result = filter_actions(
483 actions_schema! {
484 "1" => "p1", Some("ignore".to_string()),
485 "2" => "p2", Some("tag1".to_string()),
486 "3" => "p3", Some("tag2".to_string()),
487 "4" => "p4", Some("tag2".to_string())
488 },
489 &ActionTagDirective::Include(vec!["tag1".to_string(), "tag2".to_string()]),
490 );
491 assert_eq!(result.len(), 3);
492 assert_has_action!(result, "2", "p2");
493 assert_has_action!(result, "3", "p3");
494 assert_has_action!(result, "4", "p4");
495 }
496
497 #[fuchsia::test]
498 fn filter_actions_exclude_one_tag() {
499 let result = filter_actions(
500 actions_schema! {
501 "1" => "p1", Some("ignore".to_string()),
502 "2" => "p2", Some("tag".to_string()),
503 "3" => "p3", Some("tag".to_string())
504 },
505 &ActionTagDirective::Exclude(vec!["tag".to_string()]),
506 );
507 assert_eq!(result.len(), 1);
508 assert_has_action!(result, "1", "p1");
509 }
510
511 #[fuchsia::test]
512 fn filter_actions_exclude_many() {
513 let result = filter_actions(
514 actions_schema! {
515 "1" => "p1", Some("ignore".to_string()),
516 "2" => "p2", Some("tag1".to_string()),
517 "3" => "p3", Some("tag2".to_string()),
518 "4" => "p4", Some("tag2".to_string())
519 },
520 &ActionTagDirective::Exclude(vec!["tag1".to_string(), "tag2".to_string()]),
521 );
522 assert_eq!(result.len(), 1);
523 assert_has_action!(result, "1", "p1");
524 }
525
526 #[fuchsia::test]
527 fn filter_actions_include_does_not_include_empty_tag() {
528 let result = filter_actions(
529 actions_schema! {
530 "1" => "p1", None,
531 "2" => "p2", Some("tag".to_string())
532 },
533 &ActionTagDirective::Include(vec!["tag".to_string()]),
534 );
535 assert_eq!(result.len(), 1);
536 assert_has_action!(result, "2", "p2");
537 }
538
539 #[fuchsia::test]
540 fn filter_actions_exclude_does_include_empty_tag() {
541 let result = filter_actions(
542 actions_schema! {
543 "1" => "p1", None,
544 "2" => "p2", Some("tag".to_string())
545 },
546 &ActionTagDirective::Exclude(vec!["tag".to_string()]),
547 );
548 assert_eq!(result.len(), 1);
549 assert_has_action!(result, "1", "p1");
550 }
551
552 #[fuchsia::test]
553 fn select_section_parsing() {
554 let config_result = ConfigFileSchema::try_from(
555 r#"
556 {
557 select: {
558 a: ["INSPECT:core:root:prop"],
559 b: "INSPECT:core:root:prop",
560 c: ["INSPECT:core:root:prop",
561 "INSPECT:core:root:prop2",
562 "INSPECT:core:root:prop3"],
563 }
564 }
565 "#
566 .to_string(),
567 );
568 assert_eq!(
569 3,
570 config_result.expect("parse json").file_selectors.expect("has selectors").len()
571 );
572
573 let config_result = ConfigFileSchema::try_from(
574 r#"
575 {
576 select: {
577 a: ["INSPECT:core:root:prop", 1],
578 }
579 }
580 "#
581 .to_string(),
582 );
583 assert!(format!("{}", config_result.expect_err("parsing should fail"))
584 .contains("expected a string"));
585
586 let config_result = ConfigFileSchema::try_from(
587 r#"
588 {
589 select: {
590 a: [],
591 }
592 }
593 "#
594 .to_string(),
595 );
596 assert!(format!("{}", config_result.expect_err("parsing should fail"))
597 .contains("expected at least one selector"));
598 }
599
600 #[fuchsia::test]
601 fn all_selectors_works() {
602 macro_rules! s {
603 ($s:expr) => {
604 $s.to_string()
605 };
606 }
607 let file_map = hashmap![
608 s!("file1") => s!(r#"{ select: {selector1: "INSPECT:name:path:label"}}"#),
609 s!("file2") =>
610 s!(r#"
611 { select: {
612 selector1: "INSPECT:word:stuff:identifier",
613 selector2: ["INSPECT:a:b:c", "INSPECT:d:e:f"],
614 },
615 eval: {e: "2+2"} }"#),
616 ];
617 let parse = ParseResult::new(&file_map, &ActionTagDirective::AllowAll).unwrap();
618 let selectors = parse.all_selectors();
619 assert_eq!(selectors.len(), 4);
620 assert!(selectors.contains(&s!("INSPECT:name:path:label")));
621 assert!(selectors.contains(&s!("INSPECT:word:stuff:identifier")));
622 assert!(selectors.contains(&s!("INSPECT:a:b:c")));
623 assert!(selectors.contains(&s!("INSPECT:d:e:f")));
624 assert!(!selectors.contains(&s!("2+2")));
626 }
627
628 #[fuchsia::test]
629 fn too_long_signature_rejected() {
630 macro_rules! s {
631 ($s:expr) => {
632 $s.to_string()
633 };
634 }
635 let signature_ok_config = format!(
637 r#"{{
638 act: {{ foo: {{
639 type: "Snapshot",
640 repeat: "1",
641 trigger: "1>0",
642 signature: "{:a<1$}",
643 }} }}
644 }} "#,
645 "", MAX_CRASH_SIGNATURE_LENGTH as usize
647 );
648 let signature_too_long_config = format!(
649 r#"{{
650 act: {{ foo: {{
651 type: "Snapshot",
652 repeat: "1",
653 trigger: "1>0",
654 signature: "{:a<1$}",
655 }} }}
656 }} "#,
657 "", MAX_CRASH_SIGNATURE_LENGTH as usize + 1
659 );
660 let file_map_ok = hashmap![s!("file") => signature_ok_config];
661 let file_map_err = hashmap![s!("file") => signature_too_long_config];
662 assert!(ParseResult::new(&file_map_ok, &ActionTagDirective::AllowAll).is_ok());
663 assert!(ParseResult::new(&file_map_err, &ActionTagDirective::AllowAll).is_err());
664 }
665}