1 // This Source Code Form is subject to the terms of the Mozilla Public
2 // License, v. 2.0. If a copy of the MPL was not distributed with this
3 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 
5 //! # Debug options
6 //!
7 //! The debug options for Glean may be set by calling one of the `set_*` functions
8 //! or by setting specific environment variables.
9 //!
10 //! The environment variables will be read only once when the options are initialized.
11 //!
12 //! The possible debugging features available out of the box are:
13 //!
14 //! * **Ping logging** - logging the contents of ping requests that are correctly assembled;
15 //!         This may be set by calling glean.set_log_pings(value: bool)
16 //!         or by setting the environment variable GLEAN_LOG_PINGS="true";
17 //! * **Debug tagging** - Adding the X-Debug-ID header to every ping request,
18 //!         allowing these tagged pings to be sent to the ["Ping Debug Viewer"](https://mozilla.github.io/glean/book/dev/core/internal/debug-pings.html).
19 //!         This may be set by calling glean.set_debug_view_tag(value: &str)
20 //!         or by setting the environment variable GLEAN_DEBUG_VIEW_TAG=<some tag>;
21 //! * **Source tagging** - Adding the X-Source-Tags header to every ping request,
22 //!         allowing pings to be tagged with custom labels.
23 //!         This may be set by calling glean.set_source_tags(value: Vec<String>)
24 //!         or by setting the environment variable GLEAN_SOURCE_TAGS=<some, tags>;
25 //!
26 //! Bindings may implement other debugging features, e.g. sending pings on demand.
27 
28 use std::env;
29 
30 const GLEAN_LOG_PINGS: &str = "GLEAN_LOG_PINGS";
31 const GLEAN_DEBUG_VIEW_TAG: &str = "GLEAN_DEBUG_VIEW_TAG";
32 const GLEAN_SOURCE_TAGS: &str = "GLEAN_SOURCE_TAGS";
33 const GLEAN_MAX_SOURCE_TAGS: usize = 5;
34 
35 /// A representation of all of Glean's debug options.
36 pub struct DebugOptions {
37     /// Option to log the payload of pings that are successfully assembled into a ping request.
38     pub log_pings: DebugOption<bool>,
39     /// Option to add the X-Debug-ID header to every ping request.
40     pub debug_view_tag: DebugOption<String>,
41     /// Option to add the X-Source-Tags header to ping requests. This will allow the data
42     /// consumers to classify data depending on the applied tags.
43     pub source_tags: DebugOption<Vec<String>>,
44 }
45 
46 impl std::fmt::Debug for DebugOptions {
fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result47     fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
48         fmt.debug_struct("DebugOptions")
49             .field("log_pings", &self.log_pings.get())
50             .field("debug_view_tag", &self.debug_view_tag.get())
51             .field("source_tags", &self.source_tags.get())
52             .finish()
53     }
54 }
55 
56 impl DebugOptions {
new() -> Self57     pub fn new() -> Self {
58         Self {
59             log_pings: DebugOption::new(GLEAN_LOG_PINGS, get_bool_from_str, None),
60             debug_view_tag: DebugOption::new(GLEAN_DEBUG_VIEW_TAG, Some, Some(validate_tag)),
61             source_tags: DebugOption::new(
62                 GLEAN_SOURCE_TAGS,
63                 tokenize_string,
64                 Some(validate_source_tags),
65             ),
66         }
67     }
68 }
69 
70 /// A representation of a debug option,
71 /// where the value can be set programmatically or come from an environment variable.
72 #[derive(Debug)]
73 pub struct DebugOption<T, E = fn(String) -> Option<T>, V = fn(&T) -> bool> {
74     /// The name of the environment variable related to this debug option.
75     env: String,
76     /// The actual value of this option.
77     value: Option<T>,
78     /// Function to extract the data of type `T` from a `String`, used when
79     /// extracting data from the environment.
80     extraction: E,
81     /// Optional function to validate the value parsed from the environment
82     /// or passed to the `set` function.
83     validation: Option<V>,
84 }
85 
86 impl<T, E, V> DebugOption<T, E, V>
87 where
88     T: Clone,
89     E: Fn(String) -> Option<T>,
90     V: Fn(&T) -> bool,
91 {
92     /// Creates a new debug option.
93     ///
94     /// Tries to get the initial value of the option from the environment.
new(env: &str, extraction: E, validation: Option<V>) -> Self95     pub fn new(env: &str, extraction: E, validation: Option<V>) -> Self {
96         let mut option = Self {
97             env: env.into(),
98             value: None,
99             extraction,
100             validation,
101         };
102 
103         option.set_from_env();
104         option
105     }
106 
validate(&self, value: &T) -> bool107     fn validate(&self, value: &T) -> bool {
108         if let Some(f) = self.validation.as_ref() {
109             f(value)
110         } else {
111             true
112         }
113     }
114 
set_from_env(&mut self)115     fn set_from_env(&mut self) {
116         let extract = &self.extraction;
117         match env::var(&self.env) {
118             Ok(env_value) => match extract(env_value.clone()) {
119                 Some(v) => {
120                     self.set(v);
121                 }
122                 None => {
123                     log::error!(
124                         "Unable to parse debug option {}={} into {}. Ignoring.",
125                         self.env,
126                         env_value,
127                         std::any::type_name::<T>()
128                     );
129                 }
130             },
131             Err(env::VarError::NotUnicode(_)) => {
132                 log::error!("The value of {} is not valid unicode. Ignoring.", self.env)
133             }
134             // The other possible error is that the env var is not set,
135             // which is not an error for us and can safely be ignored.
136             Err(_) => {}
137         }
138     }
139 
140     /// Tries to set a value for this debug option.
141     ///
142     /// Validates the value in case a validation function is available.
143     ///
144     /// # Returns
145     ///
146     /// Whether the option passed validation and was succesfully set.
set(&mut self, value: T) -> bool147     pub fn set(&mut self, value: T) -> bool {
148         let validated = self.validate(&value);
149         if validated {
150             log::info!("Setting the debug option {}.", self.env);
151             self.value = Some(value);
152             return true;
153         }
154         log::error!("Invalid value for debug option {}.", self.env);
155         false
156     }
157 
158     /// Gets the value of this debug option.
get(&self) -> Option<&T>159     pub fn get(&self) -> Option<&T> {
160         self.value.as_ref()
161     }
162 }
163 
get_bool_from_str(value: String) -> Option<bool>164 fn get_bool_from_str(value: String) -> Option<bool> {
165     std::str::FromStr::from_str(&value).ok()
166 }
167 
tokenize_string(value: String) -> Option<Vec<String>>168 fn tokenize_string(value: String) -> Option<Vec<String>> {
169     let trimmed = value.trim();
170     if trimmed.is_empty() {
171         return None;
172     }
173 
174     Some(trimmed.split(',').map(|s| s.trim().to_string()).collect())
175 }
176 
177 /// A tag is the value used in both the `X-Debug-ID` and `X-Source-Tags` headers
178 /// of tagged ping requests, thus is it must be a valid header value.
179 ///
180 /// In other words, it must match the regex: "[a-zA-Z0-9-]{1,20}"
181 ///
182 /// The regex crate isn't used here because it adds to the binary size,
183 /// and the Glean SDK doesn't use regular expressions anywhere else.
184 #[allow(clippy::ptr_arg)]
validate_tag(value: &String) -> bool185 fn validate_tag(value: &String) -> bool {
186     if value.is_empty() {
187         log::error!("A tag must have at least one character.");
188         return false;
189     }
190 
191     let mut iter = value.chars();
192     let mut count = 0;
193 
194     loop {
195         match iter.next() {
196             // We are done, so the whole expression is valid.
197             None => return true,
198             // Valid characters.
199             Some('-') | Some('a'..='z') | Some('A'..='Z') | Some('0'..='9') => (),
200             // An invalid character
201             Some(c) => {
202                 log::error!("Invalid character '{}' in the tag.", c);
203                 return false;
204             }
205         }
206         count += 1;
207         if count == 20 {
208             log::error!("A tag cannot exceed 20 characters.");
209             return false;
210         }
211     }
212 }
213 
214 /// Validate the list of source tags.
215 ///
216 /// This builds upon the existing `validate_tag` function, since all the
217 /// tags should respect the same rules to make the pipeline happy.
218 #[allow(clippy::ptr_arg)]
validate_source_tags(tags: &Vec<String>) -> bool219 fn validate_source_tags(tags: &Vec<String>) -> bool {
220     if tags.is_empty() {
221         return false;
222     }
223 
224     if tags.len() > GLEAN_MAX_SOURCE_TAGS {
225         log::error!(
226             "A list of tags cannot contain more than {} elements.",
227             GLEAN_MAX_SOURCE_TAGS
228         );
229         return false;
230     }
231 
232     if tags.iter().any(|s| s.starts_with("glean")) {
233         log::error!("Tags starting with `glean` are reserved and must not be used.");
234         return false;
235     }
236 
237     tags.iter().all(validate_tag)
238 }
239 
240 #[cfg(test)]
241 mod test {
242     use super::*;
243     use std::env;
244 
245     #[test]
debug_option_is_correctly_loaded_from_env()246     fn debug_option_is_correctly_loaded_from_env() {
247         env::set_var("GLEAN_TEST_1", "test");
248         let option: DebugOption<String> = DebugOption::new("GLEAN_TEST_1", Some, None);
249         assert_eq!(option.get().unwrap(), "test");
250     }
251 
252     #[test]
debug_option_is_correctly_validated_when_necessary()253     fn debug_option_is_correctly_validated_when_necessary() {
254         #[allow(clippy::ptr_arg)]
255         fn validate(value: &String) -> bool {
256             value == "test"
257         }
258 
259         // Invalid values from the env are not set
260         env::set_var("GLEAN_TEST_2", "invalid");
261         let mut option: DebugOption<String> =
262             DebugOption::new("GLEAN_TEST_2", Some, Some(validate));
263         assert!(option.get().is_none());
264 
265         // Valid values are set using the `set` function
266         assert!(option.set("test".into()));
267         assert_eq!(option.get().unwrap(), "test");
268 
269         // Invalid values are not set using the `set` function
270         assert!(!option.set("invalid".into()));
271         assert_eq!(option.get().unwrap(), "test");
272     }
273 
274     #[test]
tokenize_string_splits_correctly()275     fn tokenize_string_splits_correctly() {
276         // Valid list is properly tokenized and spaces are trimmed.
277         assert_eq!(
278             Some(vec!["test1".to_string(), "test2".to_string()]),
279             tokenize_string("    test1,        test2  ".to_string())
280         );
281 
282         // Empty strings return no item.
283         assert_eq!(None, tokenize_string("".to_string()));
284     }
285 
286     #[test]
validates_tag_correctly()287     fn validates_tag_correctly() {
288         assert!(validate_tag(&"valid-value".to_string()));
289         assert!(validate_tag(&"-also-valid-value".to_string()));
290         assert!(!validate_tag(&"invalid_value".to_string()));
291         assert!(!validate_tag(&"invalid value".to_string()));
292         assert!(!validate_tag(&"!nv@lid-val*e".to_string()));
293         assert!(!validate_tag(
294             &"invalid-value-because-way-too-long".to_string()
295         ));
296         assert!(!validate_tag(&"".to_string()));
297     }
298 
299     #[test]
validates_source_tags_correctly()300     fn validates_source_tags_correctly() {
301         // Empty tags.
302         assert!(!validate_source_tags(&vec!["".to_string()]));
303         // Too many tags.
304         assert!(!validate_source_tags(&vec![
305             "1".to_string(),
306             "2".to_string(),
307             "3".to_string(),
308             "4".to_string(),
309             "5".to_string(),
310             "6".to_string()
311         ]));
312         // Invalid tags.
313         assert!(!validate_source_tags(&vec!["!nv@lid-val*e".to_string()]));
314         assert!(!validate_source_tags(&vec![
315             "glean-test1".to_string(),
316             "test2".to_string()
317         ]));
318     }
319 }
320