blob: c7b2f2ae87391b9f01add781c9c6e5ad27ef3a76 [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.
use {
anyhow::{Context as _, Error},
fidl_fuchsia_diagnostics::Selector,
fuchsia_inspect as inspect,
selectors::{contains_recursive_glob, parse_selector_file},
serde::Deserialize,
std::path::{Path, PathBuf},
std::{collections::BTreeMap, fs},
};
static DISABLE_FILTER_FILE_NAME: &'static str = "DISABLE_FILTERING.txt";
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct Config {
/// Path to which archived data will be written. No storage will be performed if left empty.
pub archive_path: Option<PathBuf>,
/// The maximum size the archive can be.
pub max_archive_size_bytes: u64,
/// The maximum size of a single event file group.
pub max_event_group_size_bytes: u64,
/// Number of threads the archivist has available to use.
pub num_threads: usize,
/// Configuration for Archivist's log subsystem.
pub logs: LogsConfig,
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct LogsConfig {
/// The maximum number of "raw logs bytes" Archivist will keep cached at one time.
///
/// Note: because the Archivist does not preserve the original messages' bytes, the amount of
/// memory consumed by the cache will be a multiple of this value. See https://fxbug.dev/67022
/// for more information and future work.
pub max_cached_original_bytes: usize,
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct ServiceConfig {
/// The list of services to connect to at startup.
///
/// Archivist is responsible for starting up diagnostics processing components listed here.
pub service_list: Vec<String>,
}
/// Configuration for pipeline selection.
pub struct PipelineConfig {
/// Map of file paths for inspect pipeline configurations to the number of selectors they
/// contain.
inspect_configs: BTreeMap<PathBuf, usize>,
/// The selectors parsed from this config.
inspect_selectors: Option<Vec<Selector>>,
/// Accumulated errors from reading config files.
errors: Vec<String>,
/// If true, filtering is disabled for this pipeline.
/// The selector files will still be parsed and verified, but they will not be applied to
/// returned data.
pub disable_filtering: bool,
}
/// Configures behavior if no configuration files are found for the pipeline.
#[derive(PartialEq)]
pub enum EmptyBehavior {
/// Disable the pipeline if no configuration files are found.
Disable,
/// Show unfiltered results if no configuration files are found.
DoNotFilter,
}
impl PipelineConfig {
/// Read a pipeline config from the given directory.
///
/// empty_behavior instructs this config on what to do when there are no configuration files
/// found.
pub fn from_directory(dir: impl AsRef<Path>, empty_behavior: EmptyBehavior) -> Self {
let suffix = std::ffi::OsStr::new("cfg");
let disable_filter_file_name = std::ffi::OsStr::new(DISABLE_FILTER_FILE_NAME);
let mut inspect_configs = BTreeMap::new();
let mut inspect_selectors = Some(vec![]);
let mut errors = vec![];
let mut disable_filtering = false;
let readdir = dir.as_ref().read_dir();
match readdir {
Err(_) => {
errors.push(format!("Failed to read directory {}", dir.as_ref().to_string_lossy()));
}
Ok(mut readdir) => {
while let Some(Ok(entry)) = readdir.next() {
let path = entry.path();
if path.extension() == Some(&suffix) {
match parse_selector_file(&path) {
Ok(selectors) => {
let mut validated_selectors = vec![];
for selector in selectors.into_iter() {
match validate_static_selector(&selector) {
Ok(()) => validated_selectors.push(selector),
Err(e) => {
errors.push(format!("Invalid static selector: {}", e))
}
}
}
inspect_configs.insert(path, validated_selectors.len());
inspect_selectors.as_mut().unwrap().extend(validated_selectors);
}
Err(e) => {
errors.push(format!(
"Failed to parse {}: {}",
path.to_string_lossy(),
e.to_string()
));
}
}
} else if path.file_name() == Some(&disable_filter_file_name) {
disable_filtering = true;
}
}
}
}
if inspect_configs.is_empty() && empty_behavior == EmptyBehavior::DoNotFilter {
inspect_selectors = None;
disable_filtering = true;
}
Self { inspect_configs, inspect_selectors, errors, disable_filtering }
}
/// Take the inspect selectors from this pipeline config.
pub fn take_inspect_selectors(&mut self) -> Option<Vec<Selector>> {
self.inspect_selectors.take()
}
/// Record stats about this pipeline config to an Inspect Node.
pub fn record_to_inspect(&self, node: &inspect::Node) {
node.record_bool("filtering_enabled", !self.disable_filtering);
let files = node.create_child("config_files");
let mut selector_sum = 0;
for (name, count) in self.inspect_configs.iter() {
let c = files.create_child(name.file_stem().unwrap_or_default().to_string_lossy());
c.record_uint("selector_count", *count as u64);
files.record(c);
selector_sum += count;
}
node.record(files);
node.record_uint("selector_count", selector_sum as u64);
if self.errors.len() != 0 {
let errors = node.create_child("errors");
for (i, error) in self.errors.iter().enumerate() {
let error_node = errors.create_child(format!("{}", i));
error_node.record_string("message", error);
errors.record(error_node);
}
node.record(errors);
}
}
/// Returns true if this pipeline config had errors.
pub fn has_error(&self) -> bool {
self.errors.len() > 0
}
}
pub fn parse_config(path: impl AsRef<Path>) -> Result<Config, Error> {
let path = path.as_ref();
let json_string: String =
fs::read_to_string(path).with_context(|| format!("parsing config: {}", path.display()))?;
let config: Config = serde_json::from_str(&json_string).context("parsing json config")?;
Ok(config)
}
pub fn parse_service_config(path: impl AsRef<Path>) -> Result<ServiceConfig, Error> {
let path = path.as_ref();
let json_string: String = fs::read_to_string(path)
.with_context(|| format!("parsing service config: {}", path.display()))?;
let config: ServiceConfig =
serde_json::from_str(&json_string).context("parsing json service config")?;
Ok(config)
}
/// Validates a static selector against rules that apply specifically to a static selector and
/// do not apply to selectors in general. Assumes the selector is already validated against the
/// rules in selectors::validate_selector.
fn validate_static_selector(static_selector: &Selector) -> Result<(), String> {
match static_selector.component_selector.as_ref() {
Some(selector) if contains_recursive_glob(selector) => {
Err(format!("Recursive glob not allowed in static selector configs"))
}
Some(_) => Ok(()),
None => Err(format!("A selector does not contain a component selector")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuchsia_inspect::testing::{assert_inspect_tree, AnyProperty};
use std::io::Write;
use std::path::Path;
fn write_test_config_to_file<T: AsRef<Path>>(path: T, test_config: &str) {
let mut file = fs::File::create(path).expect("failed to create file");
write!(file, "{}", test_config).expect("failed to write file");
file.sync_all().expect("failed to sync file");
}
#[test]
fn parse_valid_config() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
"logs": {
"max_cached_original_bytes": 500
},
"max_archive_size_bytes": 10485760,
"max_event_group_size_bytes": 262144,
"num_threads": 4
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config = parse_config(&test_config_file_name).unwrap();
assert_eq!(parsed_config.logs.max_cached_original_bytes, 500);
assert_eq!(parsed_config.max_archive_size_bytes, 10485760);
assert_eq!(parsed_config.max_event_group_size_bytes, 262144);
assert_eq!(parsed_config.num_threads, 4);
}
#[test]
fn parse_valid_config_missing_optional() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
"logs": {
"max_cached_original_bytes": 500
},
"max_archive_size_bytes": 10485760,
"max_event_group_size_bytes": 262144,
"num_threads": 1
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config = parse_config(&test_config_file_name).unwrap();
assert_eq!(parsed_config.logs.max_cached_original_bytes, 500);
assert_eq!(parsed_config.max_archive_size_bytes, 10485760);
assert_eq!(parsed_config.max_event_group_size_bytes, 262144);
assert_eq!(parsed_config.num_threads, 1);
}
#[test]
fn parse_invalid_config() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
"max_archive_size_bytes": 10485760,
"bad_field": "hello world",
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config_result = parse_config(&test_config_file_name);
assert!(parsed_config_result.is_err(), "Config had a missing field, and invalid field.");
}
#[test]
fn parse_valid_services_config() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
"service_list": ["a", "b", "c"]
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
let parsed_config_result =
parse_service_config(&test_config_file_name).expect("failed to parse config");
assert_eq!(
parsed_config_result.service_list,
vec!["a", "b", "c"].into_iter().map(|s| s.to_string()).collect::<Vec<_>>()
);
}
#[test]
fn parse_invalid_services_config() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let test_config_file_name = config_path.join("test_config.json");
let test_config = r#"
{
"service_list": [1]
}"#;
write_test_config_to_file(&test_config_file_name, test_config);
assert!(parse_service_config(&test_config_file_name).is_err());
}
#[test]
fn parse_missing_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let config = PipelineConfig::from_directory("config/missing", EmptyBehavior::Disable);
assert!(config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 0u64,
errors: {
"0": {
message: "Failed to read directory config/missing"
}
},
config_files: {}
});
assert!(!config.disable_filtering);
}
#[test]
fn parse_partially_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("ok.cfg"), "my_component.cmx:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(config_path.join("bad.cfg"), "This file fails to parse").unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
assert!(config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 1u64,
errors: {
"0": {
message: AnyProperty
}
},
config_files: {
ok: {
selector_count: 1u64
},
}
});
assert!(!config.disable_filtering);
assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
}
#[test]
fn parse_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("ok.cfg"), "my_component.cmx:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(
config_path.join("also_ok.cfg"),
"my_component.cmx:root:a\nmy_component.cmx:root/b:c\n",
)
.unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
assert!(!config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 3u64,
config_files: {
ok: {
selector_count: 1u64,
},
also_ok: {
selector_count: 2u64,
}
}
});
assert!(!config.disable_filtering);
assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
}
#[test]
fn parse_allow_empty_pipeline() {
// If a pipeline is left unconfigured, do not filter results for the pipeline.
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::DoNotFilter);
assert!(!config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
filtering_enabled: false,
selector_count: 0u64,
config_files: {
}
});
assert!(config.disable_filtering);
assert_eq!(None, config.take_inspect_selectors());
}
#[test]
fn parse_disabled_valid_pipeline() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("DISABLE_FILTERING.txt"), "This file disables filtering.")
.unwrap();
fs::write(config_path.join("ok.cfg"), "my_component.cmx:root:status").unwrap();
fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap();
fs::write(
config_path.join("also_ok.cfg"),
"my_component.cmx:root:a\nmy_component.cmx:root/b:c\n",
)
.unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
assert!(!config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
filtering_enabled: false,
selector_count: 3u64,
config_files: {
ok: {
selector_count: 1u64,
},
also_ok: {
selector_count: 2u64,
}
}
});
assert!(config.disable_filtering);
assert_eq!(3, config.take_inspect_selectors().unwrap_or_default().len());
}
#[test]
fn parse_pipeline_disallow_recursive_glob() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("config");
fs::create_dir(&config_path).unwrap();
fs::write(config_path.join("glob.cfg"), "core/a/**:root:status").unwrap();
fs::write(config_path.join("ok.cfg"), "core/b:root:status").unwrap();
let mut config = PipelineConfig::from_directory(&config_path, EmptyBehavior::Disable);
assert!(config.has_error());
let inspector = inspect::Inspector::new();
config.record_to_inspect(inspector.root());
assert_inspect_tree!(inspector, root: {
filtering_enabled: true,
selector_count: 1u64,
errors: {
"0": {
message: AnyProperty
}
},
config_files: {
ok: {
selector_count: 1u64,
},
glob: {
selector_count: 0u64,
}
}
});
assert!(!config.disable_filtering);
assert_eq!(1, config.take_inspect_selectors().unwrap_or_default().len());
}
}