summaryrefslogtreecommitdiffstats
path: root/third_party/rust/glean-core/src/debug.rs
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/glean-core/src/debug.rs')
-rw-r--r--third_party/rust/glean-core/src/debug.rs319
1 files changed, 319 insertions, 0 deletions
diff --git a/third_party/rust/glean-core/src/debug.rs b/third_party/rust/glean-core/src/debug.rs
new file mode 100644
index 0000000000..a572a02b8f
--- /dev/null
+++ b/third_party/rust/glean-core/src/debug.rs
@@ -0,0 +1,319 @@
+// 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=<some 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<String>)
+//! or by setting the environment variable GLEAN_SOURCE_TAGS=<some, 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<bool>,
+ /// Option to add the X-Debug-ID header to every ping request.
+ pub debug_view_tag: DebugOption<String>,
+ /// 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<Vec<String>>,
+}
+
+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<T, E = fn(String) -> Option<T>, 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<T>,
+ /// 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<V>,
+}
+
+impl<T, E, V> DebugOption<T, E, V>
+where
+ T: Clone,
+ E: Fn(String) -> Option<T>,
+ 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<V>) -> 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::<T>()
+ );
+ }
+ },
+ 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<bool> {
+ std::str::FromStr::from_str(&value).ok()
+}
+
+fn tokenize_string(value: String) -> Option<Vec<String>> {
+ 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<String>) -> 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<String> = 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<String> =
+ 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()
+ ]));
+ }
+}