| // Copyright 2020 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. |
| |
| // TODO(https://fxbug.dev/42169836): Refactor these into a generic ValueList |
| mod selector_list; |
| mod string_list; |
| |
| use { |
| anyhow::{bail, Context as _, Error}, |
| fuchsia_inspect as inspect, |
| futures::FutureExt, |
| glob::{GlobError, Paths}, |
| serde::{de::DeserializeOwned, Deserialize}, |
| std::fs, |
| std::path::{Path, PathBuf}, |
| std::sync::{Arc, Mutex}, |
| string_list::StringList, |
| }; |
| |
| pub use selector_list::{ParsedSelector, SelectorList}; |
| |
| const MONIKER_INTERPOLATION: &str = "{MONIKER}"; |
| const METRICS_INSPECT_SIZE_BYTES: usize = 1024 * 1024; // 1MiB |
| const DEFAULT_MIN_SAMPLE_RATE_SEC: i64 = 10; |
| |
| /// Configuration for a single project to map Inspect data to its Cobalt metrics. |
| #[derive(Deserialize, Debug, PartialEq)] |
| pub struct ProjectConfig { |
| /// Project ID that metrics are being sampled and forwarded on behalf of. |
| pub project_id: u32, |
| |
| // Customer ID that metrics are being sampled and forwarded on behalf of. |
| // This will default to 1 if not specified. Read it with the customer_id() function. |
| customer_id: Option<u32>, |
| |
| /// The frequency with which metrics are sampled, in seconds. |
| pub poll_rate_sec: i64, |
| |
| /// The collection of mappings from Inspect to Cobalt. |
| pub metrics: Vec<MetricConfig>, |
| |
| /// File name the struct was loaded from |
| #[serde(skip, default = "default_source_name")] |
| source_name: String, |
| } |
| |
| impl ProjectConfig { |
| /// Customer ID that metrics are being sampled and forwarded on behalf of. |
| /// This will default to 1 if not specified. |
| pub fn customer_id(&self) -> u32 { |
| self.customer_id.unwrap_or(1) |
| } |
| } |
| |
| /// Configuration for a single FIRE project template to map Inspect data to its Cobalt metrics |
| /// for all components in the ComponentIdInfo. Just like ProjectConfig except it uses MetricTemplate |
| /// instead of MetricConfig. |
| #[derive(Deserialize, Debug, PartialEq)] |
| struct ProjectTemplate { |
| /// Project ID that metrics are being sampled and forwarded on behalf of. |
| project_id: u32, |
| |
| /// Customer ID that metrics are being sampled and forwarded on behalf of. |
| /// This will default to 1 if not specified. |
| customer_id: Option<u32>, |
| |
| /// The frequency with which metrics are sampled, in seconds. |
| poll_rate_sec: i64, |
| |
| /// The collection of mappings from Inspect to Cobalt. |
| metrics: Vec<MetricTemplate>, |
| |
| /// File name the struct was loaded from |
| #[serde(skip, default = "default_source_name")] |
| source_name: String, |
| } |
| |
| fn default_source_name() -> String { |
| "<unknown>".to_string() |
| } |
| |
| /// Configuration for a single metric to map from an Inspect property |
| /// to a Cobalt metric. |
| #[derive(Clone, Deserialize, Debug, PartialEq)] |
| pub struct MetricConfig { |
| /// Selector identifying the metric to |
| /// sample via the diagnostics platform. |
| #[serde(rename = "selector")] |
| pub selectors: SelectorList, |
| /// Cobalt metric id to map the selector to. |
| pub metric_id: u32, |
| /// Data type to transform the metric to. |
| pub metric_type: DataType, |
| /// Event codes defining the dimensions of the |
| /// Cobalt metric. Note: Order matters, and |
| /// must match the order of the defined dimensions |
| /// in the Cobalt metric file. |
| pub event_codes: Vec<u32>, |
| /// Optional boolean specifying whether to upload |
| /// the specified metric only once, the first time |
| /// it becomes available to the sampler. |
| pub upload_once: Option<bool>, |
| /// Optional project id. When present this project id will be used instead of the top-level |
| /// project id. |
| // TODO(https://fxbug.dev/42071858): remove this when we support batching. |
| pub project_id: Option<u32>, |
| } |
| |
| /// Configuration for a single FIRE metric template to map from an Inspect property |
| /// to a cobalt metric. Unlike MetricConfig, selectors aren't parsed, and event_codes is |
| /// optional. |
| #[derive(Clone, Deserialize, Debug, PartialEq)] |
| struct MetricTemplate { |
| /// Selector identifying the metric to |
| /// sample via the diagnostics platform. |
| #[serde(rename = "selector")] |
| selectors: StringList, |
| /// Cobalt metric id to map the selector to. |
| metric_id: u32, |
| /// Data type to transform the metric to. |
| metric_type: DataType, |
| /// Event codes defining the dimensions of the |
| /// cobalt metric. |
| /// Notes: |
| /// - Order matters, and must match the order of the defined dimensions |
| /// in the cobalt metric file. |
| /// - The FIRE component-ID will be inserted as the first element of event_codes. |
| /// - The event_codes field may be omitted from the config file if component-ID is the only |
| /// event code. |
| event_codes: Option<Vec<u32>>, |
| /// Optional boolean specifying whether to upload |
| /// the specified metric only once, the first time |
| /// it becomes available to the sampler. |
| upload_once: Option<bool>, |
| /// Optional project id. When present this project id will be used instead of the top-level |
| /// project id. |
| // TODO(https://fxbug.dev/42071858): remove this when we support batching. |
| project_id: Option<u32>, |
| } |
| |
| /// Supported Cobalt Metric types |
| #[derive(Deserialize, Debug, PartialEq, Eq, Copy, Clone)] |
| pub enum DataType { |
| /// Maps cached diffs from Uint or Int Inspect types. |
| /// NOTE: This does not use duration tracking. Durations |
| /// are always set to 0. |
| Occurrence, |
| /// Maps raw Int Inspect types. |
| Integer, |
| /// Maps cached diffs from IntHistogram Inspect type. |
| IntHistogram, |
| /// Maps Inspect String type to StringValue (Cobalt 1.1 only). |
| String, |
| // TODO(lukenicholson): Expand sampler support for new |
| // data types. |
| // Maps raw Double Inspect types. |
| // FloatCustomEvent, |
| // Maps raw Uint Inspect types. |
| // IndexCustomEvent, |
| } |
| |
| // #[cfg(test)] won't work because it's used outside the library. |
| pub fn parse_selector_for_test(selector_str: &str) -> Option<ParsedSelector> { |
| Some(selector_list::parse_selector::<serde::de::value::Error>(selector_str).unwrap()) |
| } |
| |
| // TODO(https://fxbug.dev/42169836): Maybe refactor this into a generic ValueList - but remember that |
| // unlike StringList and SelectorList, it's not OK to have just a ComponentIdInfo in a config |
| // file - the file should always be a list even if there's just one (or zero) items. |
| #[derive(Deserialize, Debug)] |
| pub struct ComponentIdInfoList(Vec<ComponentIdInfo>); |
| |
| #[derive(Deserialize, Debug)] |
| pub struct ComponentIdInfo { |
| moniker: String, |
| id: u32, |
| /// Not used by Sampler, but we need to validate it |
| #[allow(unused)] |
| label: String, |
| } |
| |
| impl std::ops::Deref for ComponentIdInfoList { |
| type Target = Vec<ComponentIdInfo>; |
| |
| fn deref(&self) -> &Self::Target { |
| &self.0 |
| } |
| } |
| |
| impl std::ops::DerefMut for ComponentIdInfoList { |
| fn deref_mut(&mut self) -> &mut Self::Target { |
| &mut self.0 |
| } |
| } |
| |
| impl IntoIterator for ComponentIdInfoList { |
| type Item = ComponentIdInfo; |
| type IntoIter = std::vec::IntoIter<Self::Item>; |
| |
| fn into_iter(self) -> Self::IntoIter { |
| self.0.into_iter() |
| } |
| } |
| |
| trait RemembersSource { |
| fn remember_source(&mut self, _source: String); |
| } |
| |
| impl RemembersSource for ProjectConfig { |
| fn remember_source(&mut self, source: String) { |
| self.source_name = source; |
| } |
| } |
| |
| impl RemembersSource for ProjectTemplate { |
| fn remember_source(&mut self, source: String) { |
| self.source_name = source; |
| } |
| } |
| |
| impl RemembersSource for ComponentIdInfoList { |
| /// ComponentIdInfoList doesn't actually remember its source. |
| fn remember_source(&mut self, _source: String) {} |
| } |
| |
| fn paths_matching_name(path: impl AsRef<Path>, name: &str) -> Result<Paths, Error> { |
| let path = path.as_ref(); |
| let pattern = path.join(name); |
| Ok(glob::glob(&pattern.to_string_lossy())?) |
| } |
| |
| fn load_many<T: DeserializeOwned + RemembersSource>(paths: Paths) -> Result<Vec<T>, Error> { |
| paths |
| .map(|path: Result<PathBuf, GlobError>| { |
| let path = path?; |
| let json_string: String = |
| fs::read_to_string(&path).with_context(|| format!("parsing {}", path.display()))?; |
| let mut config: T = serde_json5::from_str(&json_string)?; |
| let file_name = path |
| .file_name() |
| .map(|name| name.to_string_lossy().to_string()) |
| .unwrap_or_else(default_source_name); |
| config.remember_source(file_name); |
| Ok(config) |
| }) |
| .collect::<Result<Vec<_>, _>>() |
| } |
| |
| /// Container for all configurations needed to instantiate the Sampler infrastructure. |
| /// Includes: |
| /// - Project configurations. |
| /// - Whether to configure the ArchiveReader for tests (e.g. longer timeouts) |
| /// - Minimum sample rate. |
| #[derive(Debug)] |
| pub struct SamplerConfig { |
| pub project_configs: Vec<Arc<ProjectConfig>>, |
| pub configure_reader_for_tests: bool, |
| pub minimum_sample_rate_sec: i64, |
| |
| // Used to store a lazy node that publishes data to Inspect if |
| // present. |
| inspect_node: Mutex<Option<inspect::LazyNode>>, |
| } |
| |
| /// Use this struct in a builder pattern to load the Sampler, and |
| /// optionally FIRE, configs. |
| pub struct SamplerConfigBuilder { |
| minimum_sample_rate_sec: i64, |
| configure_reader_for_tests: bool, |
| sampler_dir: Option<PathBuf>, // Not optional - load() will fail if this is not set. |
| fire_dir: Option<PathBuf>, |
| } |
| |
| impl SamplerConfigBuilder { |
| /// Call default() to start the builder. |
| pub fn default() -> Self { |
| SamplerConfigBuilder { |
| minimum_sample_rate_sec: DEFAULT_MIN_SAMPLE_RATE_SEC, |
| configure_reader_for_tests: false, |
| sampler_dir: None, |
| fire_dir: None, |
| } |
| } |
| |
| /// Optional. If not called, a default value will be used. |
| pub fn minimum_sample_rate_sec(mut self, minimum_sample_rate_sec: i64) -> Self { |
| self.minimum_sample_rate_sec = minimum_sample_rate_sec; |
| self |
| } |
| |
| /// Optional. For use in tests only. Configures ArchiveReader |
| /// (and thus ArchiveAccessor) to avoid test flakes. |
| pub fn configure_reader_for_tests(mut self, configure_reader_for_tests: bool) -> Self { |
| self.configure_reader_for_tests = configure_reader_for_tests; |
| self |
| } |
| |
| /// Required. The builder will fail without a Sampler config dir. |
| /// Calling multiple times will use only the last value. |
| pub fn sampler_dir(mut self, path: impl Into<PathBuf>) -> Self { |
| self.sampler_dir = Some(path.into()); |
| self |
| } |
| |
| /// Optional. FIRE config will be loaded if fire_dir() is called. |
| /// Calling multiple times will use only the last value. |
| pub fn fire_dir(mut self, path: impl Into<PathBuf>) -> Self { |
| self.fire_dir = Some(path.into()); |
| self |
| } |
| |
| /// Call load() after configuring the builder, to load and return SamplerConfig. |
| pub fn load(self) -> Result<Arc<SamplerConfig>, Error> { |
| if let Some(sampler_dir) = self.sampler_dir.as_ref() { |
| SamplerConfig::from_directories_internal( |
| self.minimum_sample_rate_sec, |
| self.configure_reader_for_tests, |
| sampler_dir, |
| self.fire_dir, |
| ) |
| .map(Arc::new) |
| } else { |
| bail!("sampler_dir must be configured before loading SamplerConfig") |
| } |
| } |
| } |
| |
| impl MetricConfig { |
| fn from_template(template: MetricTemplate, component: &ComponentIdInfo) -> Result<Self, Error> { |
| let MetricTemplate { |
| mut selectors, |
| event_codes, |
| metric_id, |
| metric_type, |
| upload_once, |
| project_id, |
| } = template; |
| let selectors = SelectorList( |
| selectors |
| .iter_mut() |
| .map::<Result<_, anyhow::Error>, _>(|s| { |
| let filled_template = Self::insert_moniker(s, &component.moniker)?; |
| Ok(match selector_list::parse_selector::<serde::de::value::Error>( |
| &filled_template, |
| ) { |
| Ok(selector) => Ok(Some(selector)), |
| Err(err) => Err(err), |
| }?) |
| }) |
| .collect::<Result<Vec<Option<_>>, _>>()?, |
| ); |
| let event_codes = match event_codes { |
| None => vec![component.id], |
| Some(mut codes) => { |
| codes.insert(0, component.id); |
| codes |
| } |
| }; |
| Ok(MetricConfig { event_codes, selectors, metric_id, metric_type, upload_once, project_id }) |
| } |
| |
| fn insert_moniker(template: &str, moniker: &str) -> Result<String, Error> { |
| let interpolate_position = template.find(MONIKER_INTERPOLATION); |
| let separator_position = template.find(":"); |
| // If the insert position is before the first colon, it's the selector's moniker and |
| // slashes should not be escaped. |
| // Otherwise, treat the moniker string as a single Node or Property name, |
| // and escape the appropriate characters. |
| match (interpolate_position, separator_position) { |
| (Some(i), Some(s)) if i < s => Ok(template.replace(MONIKER_INTERPOLATION, moniker)), |
| (Some(_), Some(_)) => Ok(template.replace( |
| MONIKER_INTERPOLATION, |
| &selectors::sanitize_string_for_selectors(moniker), |
| )), |
| (None, _) => { |
| bail!("{} not found in selector template {}", MONIKER_INTERPOLATION, template) |
| } |
| _ => bail!("Separator ':' not found in selector template {}", template), |
| } |
| } |
| } |
| |
| impl ProjectConfig { |
| fn from_template( |
| template: ProjectTemplate, |
| components: &Vec<ComponentIdInfo>, |
| ) -> Result<Self, Error> { |
| let ProjectTemplate { metrics, customer_id, project_id, poll_rate_sec, source_name } = |
| template; |
| let mut expanded_metrics = vec![]; |
| for component in components.iter() { |
| for metric in &metrics { |
| expanded_metrics.push(MetricConfig::from_template(metric.to_owned(), &component)?); |
| } |
| } |
| Ok(ProjectConfig { |
| metrics: expanded_metrics, |
| customer_id, |
| project_id, |
| poll_rate_sec, |
| source_name, |
| }) |
| } |
| } |
| |
| fn expand_fire_projects( |
| projects: Vec<ProjectTemplate>, |
| components: Vec<ComponentIdInfo>, |
| ) -> Result<Vec<ProjectConfig>, Error> { |
| projects |
| .into_iter() |
| .map(|project| ProjectConfig::from_template(project, &components)) |
| .collect::<Result<Vec<_>, _>>() |
| } |
| |
| impl SamplerConfig { |
| /// Parse the ProjectConfigurations for every project from config data. |
| /// If a FIRE directory is given, load FIRE data and convert it to ProjectConfig's. |
| fn from_directories_internal( |
| minimum_sample_rate_sec: i64, |
| configure_reader_for_tests: bool, |
| sampler_dir: impl AsRef<Path>, |
| fire_dir: Option<impl AsRef<Path>>, |
| ) -> Result<Self, Error> { |
| // TODO(https://fxbug.dev/42169894): Remove legacy_sampler_config_paths when |
| // all config dirs use sampler_dir/foo/*.json |
| let legacy_sampler_config_paths = paths_matching_name(&sampler_dir, "*.json")?; |
| let sampler_config_paths = paths_matching_name(&sampler_dir, "*/*.json")?; |
| let mut project_configs = load_many(sampler_config_paths)?; |
| project_configs.append(&mut load_many(legacy_sampler_config_paths)?); |
| if let Some(fire_dir) = fire_dir { |
| let fire_project_paths = paths_matching_name(&fire_dir, "*/projects/*.json5")?; |
| let fire_component_paths = paths_matching_name(&fire_dir, "*/components.json5")?; |
| let fire_project_templates = load_many(fire_project_paths)?; |
| let fire_components = load_many::<ComponentIdInfoList>(fire_component_paths)?; |
| let fire_components = |
| fire_components.into_iter().flatten().collect::<Vec<ComponentIdInfo>>(); |
| project_configs |
| .append(&mut expand_fire_projects(fire_project_templates, fire_components)?); |
| } |
| let project_configs = project_configs.into_iter().map(Arc::new).collect(); |
| Ok(Self { |
| minimum_sample_rate_sec, |
| project_configs, |
| configure_reader_for_tests, |
| inspect_node: Mutex::new(None), |
| }) |
| } |
| |
| /// Publish data about this config to Inspect under the given node. |
| /// |
| /// Takes an Arc so that a weak reference can be used to populate an |
| /// Inspector on demand. |
| pub fn publish_inspect(self: &Arc<Self>, parent_node: &inspect::Node) { |
| let weak_self = Arc::downgrade(self); |
| |
| lazy_static::lazy_static! { |
| static ref SELECTOR_STRING : inspect::StringReference = "selector".into(); |
| static ref UPLOAD_COUNT_STRING : inspect::StringReference = "upload_count".into(); |
| } |
| |
| let mut locked_node = self.inspect_node.lock().unwrap(); |
| |
| *locked_node = Some(parent_node.create_lazy_child("metrics_sent", move || { |
| let local_self = weak_self.upgrade(); |
| if local_self.is_none() { |
| return async move { Ok(inspect::Inspector::default()) }.boxed(); |
| } |
| |
| let local_self = local_self.unwrap(); |
| |
| let inspector = inspect::Inspector::new( |
| inspect::InspectorConfig::default().size(METRICS_INSPECT_SIZE_BYTES), |
| ); |
| let top_node = inspector.root(); |
| |
| for config in local_self.project_configs.iter() { |
| let mut next_selector_index = 0; |
| // "<unknown>" should never happen, so it's better not to make it StringReference. |
| let source_name = config.source_name.clone(); |
| top_node.record_child(source_name, |file_node| { |
| for metric in config.metrics.iter() { |
| for selector in metric.selectors.iter() { |
| if let Some(ref selector) = selector { |
| file_node.record_child( |
| format!("{}", next_selector_index), |
| |selector_node| { |
| next_selector_index += 1; |
| selector_node.record_string( |
| &*SELECTOR_STRING, |
| selector.selector_string.clone(), |
| ); |
| selector_node.record_uint( |
| &*UPLOAD_COUNT_STRING, |
| selector.get_upload_count(), |
| ); |
| }, |
| ); |
| } |
| } |
| } |
| }); |
| } |
| |
| async move { Ok(inspector) }.boxed() |
| })); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::SamplerConfigBuilder; |
| use std::fs; |
| |
| #[fuchsia::test] |
| fn parse_valid_sampler_configs() { |
| let dir = tempfile::tempdir().unwrap(); |
| let load_path = dir.path(); |
| let config_path = load_path.join("config"); |
| fs::create_dir(&config_path).unwrap(); |
| fs::write(config_path.join("ok.json"), r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| // Test comment for json5 portability. |
| "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0] |
| } |
| ] |
| } |
| "#).unwrap(); |
| fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap(); |
| fs::write( |
| config_path.join("also_ok.json"), |
| r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 3, |
| "metrics": [ |
| { |
| "selector": "single_counter_test_component:root:counter", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0] |
| } |
| ] |
| } |
| "#, |
| ) |
| .unwrap(); |
| |
| let config = SamplerConfigBuilder::default() |
| .minimum_sample_rate_sec(10) |
| .sampler_dir(&load_path) |
| .load(); |
| assert!(config.is_ok()); |
| assert_eq!(config.unwrap().project_configs.len(), 2); |
| } |
| |
| #[fuchsia::test] |
| fn parse_one_valid_one_invalid_config() { |
| let dir = tempfile::tempdir().unwrap(); |
| let load_path = dir.path(); |
| let config_path = load_path.join("config"); |
| fs::create_dir(&config_path).unwrap(); |
| fs::write(config_path.join("ok.json"), r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| // Test comment for json5 portability. |
| "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0] |
| } |
| ] |
| } |
| "#).unwrap(); |
| fs::write(config_path.join("ignored.txt"), "This file is ignored").unwrap(); |
| fs::write( |
| config_path.join("invalid.json"), |
| r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 3, |
| "invalid_field": "bad bad bad" |
| } |
| "#, |
| ) |
| .unwrap(); |
| |
| let config = SamplerConfigBuilder::default() |
| .minimum_sample_rate_sec(10) |
| .sampler_dir(&load_path) |
| .load(); |
| assert!(config.is_err()); |
| } |
| |
| #[fuchsia::test] |
| fn parse_optional_args() { |
| let dir = tempfile::tempdir().unwrap(); |
| let load_path = dir.path(); |
| let config_path = load_path.join("config"); |
| fs::create_dir(&config_path).unwrap(); |
| fs::write(config_path.join("true.json"), r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| // Test comment for json5 portability. |
| "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0], |
| "upload_once": true, |
| } |
| ] |
| } |
| "#).unwrap(); |
| |
| fs::write( |
| config_path.join("false.json"), r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| // Test comment for json5 portability. |
| "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0], |
| "upload_once": false, |
| } |
| ] |
| } |
| "#).unwrap(); |
| |
| let config = SamplerConfigBuilder::default() |
| .minimum_sample_rate_sec(10) |
| .sampler_dir(&load_path) |
| .load(); |
| assert!(config.is_ok()); |
| assert_eq!(config.unwrap().project_configs.len(), 2); |
| } |
| |
| #[fuchsia::test] |
| fn default_customer_id() { |
| let dir = tempfile::tempdir().unwrap(); |
| let load_path = dir.path(); |
| let config_path = load_path.join("config"); |
| fs::create_dir(&config_path).unwrap(); |
| fs::write(config_path.join("1default.json"), r#"{ |
| "project_id": 5, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0] |
| } |
| ] |
| } |
| "#).unwrap(); |
| fs::write( |
| config_path.join("2with_customer_id.json"), |
| r#"{ |
| "customer_id": 6, |
| "project_id": 5, |
| "poll_rate_sec": 3, |
| "metrics": [ |
| { |
| "selector": "single_counter_test_component:root:counter", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0] |
| } |
| ] |
| } |
| "#, |
| ) |
| .unwrap(); |
| |
| let config = SamplerConfigBuilder::default() |
| .minimum_sample_rate_sec(10) |
| .sampler_dir(&load_path) |
| .load(); |
| assert!(config.is_ok()); |
| assert_eq!(config.as_ref().unwrap().project_configs.len(), 2); |
| assert_eq!(config.as_ref().unwrap().project_configs[0].customer_id(), 1); |
| assert_eq!(config.as_ref().unwrap().project_configs[1].customer_id(), 6); |
| } |
| |
| #[fuchsia::test] |
| fn fire_config_loading() { |
| let sampler_dir = tempfile::tempdir().unwrap(); |
| let sampler_load_path = sampler_dir.path(); |
| let fire_dir = tempfile::tempdir().unwrap(); |
| let fire_load_path = fire_dir.path(); |
| let sampler_config_path = sampler_load_path.join("config"); |
| let fire_config_path_1 = fire_load_path.join("config1"); |
| let fire_config_path_2 = fire_load_path.join("config2"); |
| fs::create_dir(&sampler_config_path).unwrap(); |
| fs::create_dir(&fire_config_path_1).unwrap(); |
| fs::create_dir(&fire_config_path_2).unwrap(); |
| fs::write( |
| sampler_config_path.join("some_name.json"), |
| r#"{ |
| "project_id": 5, |
| "customer_id": 6, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| "selector": "bootstrap/archivist:root/all_archive_accessor:requests", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [0, 0], |
| "project_id": 4 |
| } |
| ] |
| } |
| "#, |
| ) |
| .unwrap(); |
| fs::write( |
| fire_config_path_1.join("components.json5"), |
| r#"[ |
| { |
| "id": 42, |
| "label": "Foo_42", |
| "moniker": "core/foo42" |
| } |
| ]"#, |
| ) |
| .unwrap(); |
| fs::write( |
| fire_config_path_2.join("components.json5"), |
| r#"[ |
| { |
| id: 43, |
| label: "Bar_43", |
| moniker: "bar43", |
| }, |
| ]"#, |
| ) |
| .unwrap(); |
| fs::create_dir(fire_config_path_1.join("projects")).unwrap(); |
| fs::write( |
| fire_config_path_1.join("projects/some_name.json5"), |
| r#"{ |
| "project_id": 13, |
| "customer_id": 7, |
| "poll_rate_sec": 60, |
| "metrics": [ |
| { |
| "selector": "{MONIKER}:root/path:leaf", |
| "metric_id": 1, |
| "metric_type": "Occurrence", |
| "event_codes": [1, 2] |
| } |
| ] |
| } |
| "#, |
| ) |
| .unwrap(); |
| fs::write( |
| fire_config_path_1.join("projects/another_name.json5"), |
| r#"{ |
| "project_id": 13, |
| "poll_rate_sec": 60, |
| "customer_id": 8, |
| "metrics": [ |
| { |
| "selector": [ |
| "{MONIKER}:root/path2:leaf2", |
| "foo/bar:root/{MONIKER}:leaf3", |
| "asdf/qwer:root/path4:pre-{MONIKER}-post", |
| ], |
| "metric_id": 2, |
| "metric_type": "Occurrence", |
| } |
| ] |
| } |
| "#, |
| ) |
| .unwrap(); |
| |
| let config = SamplerConfigBuilder::default() |
| .minimum_sample_rate_sec(10) |
| .sampler_dir(&sampler_load_path) |
| .fire_dir(&fire_load_path) |
| .load(); |
| assert!(config.is_ok()); |
| let configs = &config.as_ref().unwrap().project_configs; |
| // Customer ID 6 is normal Sampler config. ID 7 and 8 are FIRE configs. There must be |
| // one project config for each customer ID, 3 total. |
| let config_6 = configs.iter().filter(|c| c.customer_id() == 6).next().unwrap(); |
| let config_7 = configs.iter().filter(|c| c.customer_id() == 7).next().unwrap(); |
| let config_8 = configs.iter().filter(|c| c.customer_id() == 8).next().unwrap(); |
| let metric_6 = |
| config_6.metrics.iter().filter(|m| m.event_codes == vec![0, 0]).next().unwrap(); |
| let metric_7_42 = |
| config_7.metrics.iter().filter(|m| m.event_codes == vec![42, 1, 2]).next().unwrap(); |
| let metric_7_43 = |
| config_7.metrics.iter().filter(|m| m.event_codes == vec![43, 1, 2]).next().unwrap(); |
| let metric_8_42 = |
| config_8.metrics.iter().filter(|m| m.event_codes == vec![42]).next().unwrap(); |
| let metric_8_43 = |
| config_8.metrics.iter().filter(|m| m.event_codes == vec![43]).next().unwrap(); |
| |
| // Make sure we don't have any extra configs or metrics |
| assert_eq!(configs.len(), 3); |
| assert_eq!(config_6.metrics.len(), 1); |
| assert_eq!(config_7.metrics.len(), 2); |
| assert_eq!(config_8.metrics.len(), 2); |
| // Make sure all metrics have the right selectors |
| assert_eq!( |
| metric_6 |
| .selectors |
| .iter() |
| .map(|s| s.as_ref().unwrap().selector_string.to_owned()) |
| .collect::<Vec<_>>(), |
| vec!["bootstrap/archivist:root/all_archive_accessor:requests"] |
| ); |
| assert_eq!(metric_6.project_id, Some(4)); |
| assert_eq!( |
| metric_7_42 |
| .selectors |
| .iter() |
| .map(|s| s.as_ref().unwrap().selector_string.to_owned()) |
| .collect::<Vec<_>>(), |
| vec!["core/foo42:root/path:leaf"] |
| ); |
| assert_eq!( |
| metric_7_43 |
| .selectors |
| .iter() |
| .map(|s| s.as_ref().unwrap().selector_string.to_owned()) |
| .collect::<Vec<_>>(), |
| vec!["bar43:root/path:leaf"] |
| ); |
| assert_eq!( |
| metric_8_42 |
| .selectors |
| .iter() |
| .map(|s| s.as_ref().unwrap().selector_string.to_owned()) |
| .collect::<Vec<_>>(), |
| vec![ |
| "core/foo42:root/path2:leaf2", |
| "foo/bar:root/core\\/foo42:leaf3", |
| "asdf/qwer:root/path4:pre-core\\/foo42-post", |
| ] |
| ); |
| assert_eq!( |
| metric_8_43 |
| .selectors |
| .iter() |
| .map(|s| s.as_ref().unwrap().selector_string.to_owned()) |
| .collect::<Vec<_>>(), |
| vec![ |
| "bar43:root/path2:leaf2", |
| "foo/bar:root/bar43:leaf3", |
| "asdf/qwer:root/path4:pre-bar43-post", |
| ] |
| ); |
| } |
| } |