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