blob: ffb6101d856ae61d08628b41034a8bc7e2834520 [file] [log] [blame]
// 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",
]
);
}
}