// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! # Debug options //! //! The debug options for Glean may be set by calling one of the `set_*` functions //! or by setting specific environment variables. //! //! The environment variables will be read only once when the options are initialized. //! //! The possible debugging features available out of the box are: //! //! * **Ping logging** - logging the contents of ping requests that are correctly assembled; //! This may be set by calling glean.set_log_pings(value: bool) //! or by setting the environment variable GLEAN_LOG_PINGS="true"; //! * **Debug tagging** - Adding the X-Debug-ID header to every ping request, //! allowing these tagged pings to be sent to the ["Ping Debug Viewer"](https://mozilla.github.io/glean/book/dev/core/internal/debug-pings.html). //! This may be set by calling glean.set_debug_view_tag(value: &str) //! or by setting the environment variable GLEAN_DEBUG_VIEW_TAG=; //! * **Source tagging** - Adding the X-Source-Tags header to every ping request, //! allowing pings to be tagged with custom labels. //! This may be set by calling glean.set_source_tags(value: Vec) //! or by setting the environment variable GLEAN_SOURCE_TAGS=; //! //! Bindings may implement other debugging features, e.g. sending pings on demand. use std::env; const GLEAN_LOG_PINGS: &str = "GLEAN_LOG_PINGS"; const GLEAN_DEBUG_VIEW_TAG: &str = "GLEAN_DEBUG_VIEW_TAG"; const GLEAN_SOURCE_TAGS: &str = "GLEAN_SOURCE_TAGS"; const GLEAN_MAX_SOURCE_TAGS: usize = 5; /// A representation of all of Glean's debug options. pub struct DebugOptions { /// Option to log the payload of pings that are successfully assembled into a ping request. pub log_pings: DebugOption, /// Option to add the X-Debug-ID header to every ping request. pub debug_view_tag: DebugOption, /// Option to add the X-Source-Tags header to ping requests. This will allow the data /// consumers to classify data depending on the applied tags. pub source_tags: DebugOption>, } impl std::fmt::Debug for DebugOptions { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.debug_struct("DebugOptions") .field("log_pings", &self.log_pings.get()) .field("debug_view_tag", &self.debug_view_tag.get()) .field("source_tags", &self.source_tags.get()) .finish() } } impl DebugOptions { pub fn new() -> Self { Self { log_pings: DebugOption::new(GLEAN_LOG_PINGS, get_bool_from_str, None), debug_view_tag: DebugOption::new(GLEAN_DEBUG_VIEW_TAG, Some, Some(validate_tag)), source_tags: DebugOption::new( GLEAN_SOURCE_TAGS, tokenize_string, Some(validate_source_tags), ), } } } /// A representation of a debug option, /// where the value can be set programmatically or come from an environment variable. #[derive(Debug)] pub struct DebugOption Option, V = fn(&T) -> bool> { /// The name of the environment variable related to this debug option. env: String, /// The actual value of this option. value: Option, /// Function to extract the data of type `T` from a `String`, used when /// extracting data from the environment. extraction: E, /// Optional function to validate the value parsed from the environment /// or passed to the `set` function. validation: Option, } impl DebugOption where T: Clone, E: Fn(String) -> Option, V: Fn(&T) -> bool, { /// Creates a new debug option. /// /// Tries to get the initial value of the option from the environment. pub fn new(env: &str, extraction: E, validation: Option) -> Self { let mut option = Self { env: env.into(), value: None, extraction, validation, }; option.set_from_env(); option } fn validate(&self, value: &T) -> bool { if let Some(f) = self.validation.as_ref() { f(value) } else { true } } fn set_from_env(&mut self) { let extract = &self.extraction; match env::var(&self.env) { Ok(env_value) => match extract(env_value.clone()) { Some(v) => { self.set(v); } None => { log::error!( "Unable to parse debug option {}={} into {}. Ignoring.", self.env, env_value, std::any::type_name::() ); } }, Err(env::VarError::NotUnicode(_)) => { log::error!("The value of {} is not valid unicode. Ignoring.", self.env) } // The other possible error is that the env var is not set, // which is not an error for us and can safely be ignored. Err(_) => {} } } /// Tries to set a value for this debug option. /// /// Validates the value in case a validation function is available. /// /// # Returns /// /// Whether the option passed validation and was succesfully set. pub fn set(&mut self, value: T) -> bool { let validated = self.validate(&value); if validated { log::info!("Setting the debug option {}.", self.env); self.value = Some(value); return true; } log::error!("Invalid value for debug option {}.", self.env); false } /// Gets the value of this debug option. pub fn get(&self) -> Option<&T> { self.value.as_ref() } } fn get_bool_from_str(value: String) -> Option { std::str::FromStr::from_str(&value).ok() } fn tokenize_string(value: String) -> Option> { let trimmed = value.trim(); if trimmed.is_empty() { return None; } Some(trimmed.split(',').map(|s| s.trim().to_string()).collect()) } /// A tag is the value used in both the `X-Debug-ID` and `X-Source-Tags` headers /// of tagged ping requests, thus is it must be a valid header value. /// /// In other words, it must match the regex: "[a-zA-Z0-9-]{1,20}" /// /// The regex crate isn't used here because it adds to the binary size, /// and the Glean SDK doesn't use regular expressions anywhere else. #[allow(clippy::ptr_arg)] fn validate_tag(value: &String) -> bool { if value.is_empty() { log::error!("A tag must have at least one character."); return false; } let mut iter = value.chars(); let mut count = 0; loop { match iter.next() { // We are done, so the whole expression is valid. None => return true, // Valid characters. Some('-') | Some('a'..='z') | Some('A'..='Z') | Some('0'..='9') => (), // An invalid character Some(c) => { log::error!("Invalid character '{}' in the tag.", c); return false; } } count += 1; if count == 20 { log::error!("A tag cannot exceed 20 characters."); return false; } } } /// Validate the list of source tags. /// /// This builds upon the existing `validate_tag` function, since all the /// tags should respect the same rules to make the pipeline happy. #[allow(clippy::ptr_arg)] fn validate_source_tags(tags: &Vec) -> bool { if tags.is_empty() { return false; } if tags.len() > GLEAN_MAX_SOURCE_TAGS { log::error!( "A list of tags cannot contain more than {} elements.", GLEAN_MAX_SOURCE_TAGS ); return false; } if tags.iter().any(|s| s.starts_with("glean")) { log::error!("Tags starting with `glean` are reserved and must not be used."); return false; } tags.iter().all(validate_tag) } #[cfg(test)] mod test { use super::*; use std::env; #[test] fn debug_option_is_correctly_loaded_from_env() { env::set_var("GLEAN_TEST_1", "test"); let option: DebugOption = DebugOption::new("GLEAN_TEST_1", Some, None); assert_eq!(option.get().unwrap(), "test"); } #[test] fn debug_option_is_correctly_validated_when_necessary() { #[allow(clippy::ptr_arg)] fn validate(value: &String) -> bool { value == "test" } // Invalid values from the env are not set env::set_var("GLEAN_TEST_2", "invalid"); let mut option: DebugOption = DebugOption::new("GLEAN_TEST_2", Some, Some(validate)); assert!(option.get().is_none()); // Valid values are set using the `set` function assert!(option.set("test".into())); assert_eq!(option.get().unwrap(), "test"); // Invalid values are not set using the `set` function assert!(!option.set("invalid".into())); assert_eq!(option.get().unwrap(), "test"); } #[test] fn tokenize_string_splits_correctly() { // Valid list is properly tokenized and spaces are trimmed. assert_eq!( Some(vec!["test1".to_string(), "test2".to_string()]), tokenize_string(" test1, test2 ".to_string()) ); // Empty strings return no item. assert_eq!(None, tokenize_string("".to_string())); } #[test] fn validates_tag_correctly() { assert!(validate_tag(&"valid-value".to_string())); assert!(validate_tag(&"-also-valid-value".to_string())); assert!(!validate_tag(&"invalid_value".to_string())); assert!(!validate_tag(&"invalid value".to_string())); assert!(!validate_tag(&"!nv@lid-val*e".to_string())); assert!(!validate_tag( &"invalid-value-because-way-too-long".to_string() )); assert!(!validate_tag(&"".to_string())); } #[test] fn validates_source_tags_correctly() { // Empty tags. assert!(!validate_source_tags(&vec!["".to_string()])); // Too many tags. assert!(!validate_source_tags(&vec![ "1".to_string(), "2".to_string(), "3".to_string(), "4".to_string(), "5".to_string(), "6".to_string() ])); // Invalid tags. assert!(!validate_source_tags(&vec!["!nv@lid-val*e".to_string()])); assert!(!validate_source_tags(&vec![ "glean-test1".to_string(), "test2".to_string() ])); } }