blob: 3e4ef4d6865cadf592a175f6e3833f6f8730f3bd [file] [log] [blame]
// Copyright 2021 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.
use {
crate::trace::duration,
anyhow::Error,
diagnostics_data::{LogTextDisplayOptions, LogTextPresenter, LogsData, Severity},
fidl_fuchsia_diagnostics::LogInterestSelector,
fidl_fuchsia_test_manager::LogsIteratorOption,
futures::{Stream, TryStreamExt},
std::io::Write,
};
/// Configuration for display of text-based (unstructured)
/// logs.
#[derive(Clone, Default)]
pub(crate) struct LogDisplayConfiguration {
/// The minimum severity log to display.
pub interest: Vec<LogInterestSelector>,
/// How to format messages as text.
pub text_options: LogTextDisplayOptions,
}
// TODO(https://fxbug.dev/42131693, https://fxbug.dev/42149841): deprecate this when implementing metadata selectors for
// logs or when we support automatically sending interest updates to all test components on startup.
// We currently don't have a way of setting the interest of a test realm before creating that realm.
#[derive(Clone, Default)]
pub(crate) struct LogCollectionOptions {
/// The maximum allowed severity for logs.
pub max_severity: Option<Severity>,
/// Log display options for unstructured logs.
pub format: LogDisplayConfiguration,
}
impl LogCollectionOptions {
fn is_restricted_log(&self, log: &LogsData) -> bool {
let severity = log.metadata.severity;
matches!(self.max_severity, Some(max) if severity > max)
}
fn should_display(&self, log: &LogsData) -> bool {
if self.format.interest.is_empty() {
return true;
}
let mut found_matching_selector = false;
for LogInterestSelector { interest, selector } in &self.format.interest {
let Some(min_severity) = interest.min_severity.as_ref() else {
continue;
};
if selectors::match_moniker_against_component_selector(
log.moniker.split('/'),
&selector,
)
// The selector should already be validated so in practice this will never happen.
.unwrap_or(false)
{
found_matching_selector = true;
if log.severity() >= *min_severity {
return true;
}
}
}
!found_matching_selector
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum LogCollectionOutcome {
Error { restricted_logs: Vec<String> },
Passed,
}
impl From<Vec<String>> for LogCollectionOutcome {
fn from(restricted_logs: Vec<String>) -> Self {
if restricted_logs.is_empty() {
LogCollectionOutcome::Passed
} else {
LogCollectionOutcome::Error { restricted_logs }
}
}
}
/// Collects logs from |stream|, filters out low severity logs, and stores the results
/// in |log_artifact|. Returns any high severity restricted logs that are encountered.
pub(crate) async fn collect_logs<S, W>(
mut stream: S,
mut log_artifact: W,
options: LogCollectionOptions,
) -> Result<LogCollectionOutcome, Error>
where
S: Stream<Item = Result<LogsData, Error>> + Unpin,
W: Write,
{
duration!(c"collect_logs");
let mut restricted_logs = vec![];
while let Some(log) = stream.try_next().await? {
duration!(c"process_single_log");
let is_restricted = options.is_restricted_log(&log);
let should_display = options.should_display(&log);
if !should_display && !is_restricted {
continue;
}
let log_repr = format!("{}", LogTextPresenter::new(&log, options.format.text_options));
if should_display {
writeln!(log_artifact, "{}", log_repr)?;
}
if is_restricted {
restricted_logs.push(log_repr);
}
}
Ok(restricted_logs.into())
}
#[cfg(target_os = "fuchsia")]
pub fn get_type() -> LogsIteratorOption {
LogsIteratorOption::BatchIterator
}
#[cfg(not(target_os = "fuchsia"))]
pub fn get_type() -> LogsIteratorOption {
LogsIteratorOption::SocketBatchIterator
}
#[cfg(test)]
mod test {
use {
super::*,
diagnostics_data::{BuilderArgs, LogsDataBuilder},
};
#[fuchsia::test]
async fn filter_low_severity() {
let input_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "child".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("my info log")
.build(),
];
let displayed_logs = vec![LogsDataBuilder::new(BuilderArgs {
moniker: "child".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("my info log")
.build()];
let mut log_artifact = vec![];
assert_eq!(
collect_logs(
futures::stream::iter(input_logs.into_iter().map(Ok)),
&mut log_artifact,
LogCollectionOptions {
max_severity: None,
format: LogDisplayConfiguration {
text_options: LogTextDisplayOptions {
show_full_moniker: true,
..Default::default()
},
interest: vec![
selectors::parse_log_interest_selector_or_severity("WARN").unwrap()
],
}
},
)
.await
.unwrap(),
LogCollectionOutcome::Passed
);
assert_eq!(
String::from_utf8(log_artifact).unwrap(),
displayed_logs.iter().map(|log| format!("{}\n", log)).collect::<Vec<_>>().concat()
);
}
#[fuchsia::test]
async fn filter_log_severity_by_component() {
let a_info_log = LogsDataBuilder::new(BuilderArgs {
moniker: "a".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("A's info log")
.build();
let a_warn_log = LogsDataBuilder::new(BuilderArgs {
moniker: "a".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("A's warn log")
.build();
let b_info_log = LogsDataBuilder::new(BuilderArgs {
moniker: "b".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("B's info log")
.build();
let b_warn_log = LogsDataBuilder::new(BuilderArgs {
moniker: "b".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("B's warn log")
.build();
let c_info_log = LogsDataBuilder::new(BuilderArgs {
moniker: "c".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("C's info log")
.build();
let input_logs = vec![
a_info_log,
a_warn_log.clone(),
b_info_log,
b_warn_log.clone(),
c_info_log.clone(),
];
let displayed_logs = vec![a_warn_log, b_warn_log, c_info_log];
let mut log_artifact = vec![];
assert_eq!(
collect_logs(
futures::stream::iter(input_logs.into_iter().map(Ok)),
&mut log_artifact,
LogCollectionOptions {
max_severity: None,
format: LogDisplayConfiguration {
text_options: LogTextDisplayOptions {
show_full_moniker: true,
..Default::default()
},
interest: vec![
selectors::parse_log_interest_selector_or_severity("a#WARN").unwrap(),
selectors::parse_log_interest_selector_or_severity("b#WARN").unwrap(),
],
}
},
)
.await
.unwrap(),
LogCollectionOutcome::Passed
);
assert_eq!(
String::from_utf8(log_artifact).unwrap(),
displayed_logs.iter().map(|log| format!("{}\n", log)).collect::<Vec<_>>().concat()
);
}
#[fuchsia::test]
async fn filter_log_severity_by_component_multiple_matches() {
let a_info_log = LogsDataBuilder::new(BuilderArgs {
moniker: "a".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("A's info log")
.build();
let a_warn_log = LogsDataBuilder::new(BuilderArgs {
moniker: "a".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("A's warn log")
.build();
let b_info_log = LogsDataBuilder::new(BuilderArgs {
moniker: "b".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("B's info log")
.build();
let b_fatal_log = LogsDataBuilder::new(BuilderArgs {
moniker: "b".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Fatal,
})
.set_message("B's fatal log")
.build();
let input_logs = vec![a_info_log, a_warn_log.clone(), b_info_log, b_fatal_log.clone()];
let displayed_logs = vec![a_warn_log, b_fatal_log];
let mut log_artifact = vec![];
assert_eq!(
collect_logs(
futures::stream::iter(input_logs.into_iter().map(Ok)),
&mut log_artifact,
LogCollectionOptions {
max_severity: None,
format: LogDisplayConfiguration {
text_options: LogTextDisplayOptions {
show_full_moniker: true,
..Default::default()
},
interest: vec![
selectors::parse_log_interest_selector_or_severity("**#FATAL").unwrap(),
selectors::parse_log_interest_selector_or_severity("a#WARN").unwrap(),
],
}
},
)
.await
.unwrap(),
LogCollectionOutcome::Passed
);
assert_eq!(
String::from_utf8(log_artifact).unwrap(),
displayed_logs.iter().map(|log| format!("{}\n", log)).collect::<Vec<_>>().concat()
);
}
#[fuchsia::test]
async fn filter_log_moniker() {
let unaltered_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>/child/a".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("my warn log")
.build(),
];
let altered_moniker_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "a".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("my warn log")
.build(),
];
let mut log_artifact = vec![];
assert_eq!(
collect_logs(
futures::stream::iter(unaltered_logs.into_iter().map(Ok)),
&mut log_artifact,
LogCollectionOptions {
max_severity: None,
format: LogDisplayConfiguration {
text_options: LogTextDisplayOptions {
show_full_moniker: false,
..Default::default()
},
interest: vec![]
}
}
)
.await
.unwrap(),
LogCollectionOutcome::Passed
);
assert_eq!(
String::from_utf8(log_artifact).unwrap(),
altered_moniker_logs
.iter()
.map(|log| format!("{}\n", LogTextPresenter::new(log, Default::default())))
.collect::<Vec<_>>()
.concat()
);
}
#[fuchsia::test]
async fn no_filter_log_moniker() {
let unaltered_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "child/a".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("my warn log")
.build(),
];
let altered_moniker_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "child/a".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Warn,
})
.set_message("my warn log")
.build(),
];
let mut log_artifact = vec![];
assert_eq!(
collect_logs(
futures::stream::iter(unaltered_logs.into_iter().map(Ok)),
&mut log_artifact,
LogCollectionOptions {
max_severity: None,
format: LogDisplayConfiguration {
text_options: LogTextDisplayOptions {
show_full_moniker: true,
..Default::default()
},
interest: vec![],
}
}
)
.await
.unwrap(),
LogCollectionOutcome::Passed
);
assert_eq!(
String::from_utf8(log_artifact).unwrap(),
altered_moniker_logs
.iter()
.map(|log| format!("{}\n", log))
.collect::<Vec<_>>()
.concat()
);
}
#[fuchsia::test]
async fn display_restricted_logs() {
let input_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "child".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Error,
})
.set_message("my error log")
.build(),
];
let displayed_logs = vec![
LogsDataBuilder::new(BuilderArgs {
moniker: "<root>".into(),
timestamp_nanos: 0i64.into(),
component_url: "test-root-url".to_string().into(),
severity: Severity::Info,
})
.set_message("my info log")
.build(),
LogsDataBuilder::new(BuilderArgs {
moniker: "child".into(),
timestamp_nanos: 1000i64.into(),
component_url: "test-child-url".to_string().into(),
severity: Severity::Error,
})
.set_message("my error log")
.build(),
];
let mut log_artifact = vec![];
assert_eq!(
collect_logs(
futures::stream::iter(input_logs.into_iter().map(Ok)),
&mut log_artifact,
LogCollectionOptions {
max_severity: Severity::Warn.into(),
format: LogDisplayConfiguration {
text_options: LogTextDisplayOptions {
show_full_moniker: true,
..Default::default()
},
interest: vec![]
}
}
)
.await
.unwrap(),
LogCollectionOutcome::Error { restricted_logs: vec![format!("{}", displayed_logs[1])] }
);
assert_eq!(
String::from_utf8(log_artifact).unwrap(),
displayed_logs.iter().map(|log| format!("{}\n", log)).collect::<Vec<_>>().concat()
);
}
}