| // 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. |
| use { |
| anyhow::{format_err, Context as _, Error}, |
| serde::Deserialize, |
| serde_json5, |
| std::fs, |
| std::path::Path, |
| }; |
| |
| /// Configuration for a single project to map inspect data to its cobalt metrics. |
| #[derive(Deserialize, Debug, PartialEq, Eq)] |
| pub struct ProjectConfig { |
| /// Project ID that metrics are being sampled and forwarded on behalf of. |
| pub project_id: 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>, |
| } |
| |
| /// Configuration for a single metric to map from an inspect property |
| /// to a cobalt metric. |
| #[derive(Deserialize, Debug, PartialEq, Eq)] |
| pub struct MetricConfig { |
| /// Selector identifying the metric to |
| /// sample via the diagnostics platform. |
| pub selector: String, |
| /// 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>, |
| } |
| |
| /// The supported V1.0 Cobalt Metrics |
| #[derive(Deserialize, Debug, PartialEq, Eq, 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. |
| EventCount, |
| // Maps raw Int inspect types. |
| Integer, |
| // TODO(lukenicholson): Expand sampler support for new |
| // data types. |
| // Maps cached diffs from IntHistogram inspect type. |
| // IntHistogram, |
| // Maps raw Double inspect types. |
| // FloatCustomEvent, |
| // Maps raw Uint inspect types. |
| // IndexCustomEvent, |
| } |
| |
| /// Parses a configuration file for a single project into a ProjectConfig. |
| pub fn parse_config(path: impl AsRef<Path>) -> Result<ProjectConfig, Error> { |
| let path = path.as_ref(); |
| let json_string: String = |
| fs::read_to_string(path).with_context(|| format!("parsing config: {}", path.display()))?; |
| let config: ProjectConfig = serde_json5::from_str(&json_string)?; |
| Ok(config) |
| } |
| |
| /// Container for all configurations needed to instantiate the Sampler infrastructure. |
| /// Includes: |
| /// - Project configurations. |
| /// - Minimum sample rate. |
| pub struct SamplerConfig { |
| pub project_configs: Vec<ProjectConfig>, |
| pub minimum_sample_rate_sec: i64, |
| } |
| |
| impl SamplerConfig { |
| /// Parse the ProjectConfigurations for every project from config data. |
| pub fn from_directory( |
| minimum_sample_rate_sec: i64, |
| dir: impl AsRef<Path>, |
| ) -> Result<Self, Error> { |
| let suffix = std::ffi::OsStr::new("json"); |
| let readdir = dir.as_ref().read_dir(); |
| let mut project_configs: Vec<ProjectConfig> = Vec::new(); |
| match readdir { |
| Err(e) => { |
| return Err(format_err!( |
| "Failed to read directory {}, Error: {:?}", |
| dir.as_ref().to_string_lossy(), |
| e |
| )); |
| } |
| Ok(mut readdir) => { |
| while let Some(Ok(entry)) = readdir.next() { |
| let path = entry.path(); |
| if path.extension() == Some(&suffix) { |
| match parse_config(&path) { |
| Ok(project_config) => { |
| project_configs.push(project_config); |
| } |
| Err(e) => { |
| return Err(format_err!( |
| "Failed to parse {}: {}", |
| path.to_string_lossy(), |
| e.to_string() |
| )); |
| } |
| } |
| } |
| } |
| } |
| } |
| Ok(Self { minimum_sample_rate_sec, project_configs }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::SamplerConfig; |
| use std::fs; |
| #[test] |
| fn parse_valid_configs() { |
| let dir = tempfile::tempdir().unwrap(); |
| let config_path = dir.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": "EventCount", |
| "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.cmx:root:counter", |
| "metric_id": 1, |
| "metric_type": "EventCount", |
| "event_codes": [0, 0] |
| } |
| ] |
| } |
| "#, |
| ) |
| .unwrap(); |
| |
| let config = SamplerConfig::from_directory(10, &config_path); |
| assert!(config.is_ok()); |
| assert_eq!(config.unwrap().project_configs.len(), 2); |
| } |
| |
| #[test] |
| fn parse_one_valid_one_invalid_config() { |
| let dir = tempfile::tempdir().unwrap(); |
| let config_path = dir.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": "EventCount", |
| "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 = SamplerConfig::from_directory(10, &config_path); |
| assert!(config.is_err()); |
| } |
| |
| #[test] |
| fn parse_optional_args() { |
| let dir = tempfile::tempdir().unwrap(); |
| let config_path = dir.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": "EventCount", |
| "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": "EventCount", |
| "event_codes": [0, 0], |
| "upload_once": false, |
| } |
| ] |
| } |
| "#).unwrap(); |
| |
| let config = SamplerConfig::from_directory(10, &config_path); |
| assert!(config.is_ok()); |
| assert_eq!(config.unwrap().project_configs.len(), 2); |
| } |
| } |