1use crate::common::{CustomerId, EventCode, MetricId, MetricType, ProjectId};
6use crate::utils::OneOrMany;
7use anyhow::bail;
8use component_id_index::InstanceId;
9use moniker::ExtendedMoniker;
10use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
11use std::marker::PhantomData;
12use std::str::FromStr;
13
14pub use crate::runtime::{MetricConfig, ProjectConfig};
17
18#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
22pub struct ProjectTemplate {
23 pub project_id: ProjectId,
25
26 #[serde(default)]
29 pub customer_id: CustomerId,
30
31 #[serde(deserialize_with = "crate::utils::greater_than_zero")]
33 pub poll_rate_sec: i64,
34
35 pub metrics: Vec<MetricTemplate>,
37}
38
39impl ProjectTemplate {
40 pub fn expand(self, components: &ComponentIdInfoList) -> Result<ProjectConfig, anyhow::Error> {
41 let ProjectTemplate { project_id, customer_id, poll_rate_sec, metrics } = self;
42 let mut metric_configs = Vec::with_capacity(metrics.len() * components.len());
43 for component in components.iter() {
44 for metric in metrics.iter() {
45 if let Some(metric_config) = metric.expand(component)? {
46 metric_configs.push(metric_config);
47 }
48 }
49 }
50 Ok(ProjectConfig { project_id, customer_id, poll_rate_sec, metrics: metric_configs })
51 }
52}
53
54#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
57pub struct MetricTemplate {
58 #[serde(rename = "selector", deserialize_with = "one_or_many_strings")]
61 pub selectors: Vec<String>,
62
63 pub metric_id: MetricId,
65
66 pub metric_type: MetricType,
68
69 #[serde(default)]
78 pub event_codes: Vec<EventCode>,
79
80 #[serde(default)]
83 pub upload_once: bool,
84
85 pub project_id: Option<ProjectId>,
89}
90
91#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
92pub struct ComponentIdInfo {
93 #[serde(deserialize_with = "moniker_deserialize", serialize_with = "moniker_serialize")]
95 pub moniker: ExtendedMoniker,
96
97 #[serde(
99 default,
100 deserialize_with = "instance_id_deserialize",
101 serialize_with = "instance_id_serialize"
102 )]
103 pub instance_id: Option<InstanceId>,
104
105 #[serde(alias = "id")]
107 pub event_id: EventCode,
108
109 pub label: String,
111}
112
113#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
114pub struct ComponentIdInfoList(Vec<ComponentIdInfo>);
115
116impl ComponentIdInfoList {
117 pub fn new(infos: Vec<ComponentIdInfo>) -> Self {
118 Self(infos)
119 }
120
121 pub fn add_instance_ids(&mut self, index: &component_id_index::Index) {
122 for component in self.0.iter_mut() {
123 if let ExtendedMoniker::ComponentInstance(moniker) = &component.moniker {
124 component.instance_id = index.id_for_moniker(moniker).cloned();
125 }
126 }
127 }
128
129 pub fn iter(&self) -> impl Iterator<Item = &ComponentIdInfo> {
130 self.0.iter()
131 }
132}
133
134impl std::ops::Deref for ComponentIdInfoList {
135 type Target = Vec<ComponentIdInfo>;
136
137 fn deref(&self) -> &Self::Target {
138 &self.0
139 }
140}
141
142impl std::ops::DerefMut for ComponentIdInfoList {
143 fn deref_mut(&mut self) -> &mut Self::Target {
144 &mut self.0
145 }
146}
147
148impl IntoIterator for ComponentIdInfoList {
149 type Item = ComponentIdInfo;
150 type IntoIter = std::vec::IntoIter<Self::Item>;
151
152 fn into_iter(self) -> Self::IntoIter {
153 self.0.into_iter()
154 }
155}
156
157impl MetricTemplate {
158 fn expand(&self, component: &ComponentIdInfo) -> Result<Option<MetricConfig>, anyhow::Error> {
159 let MetricTemplate {
160 selectors,
161 metric_id,
162 metric_type,
163 event_codes,
164 upload_once,
165 project_id,
166 } = self;
167 let mut result_selectors = Vec::with_capacity(selectors.len());
168 for selector in selectors {
169 if let Some(selector_string) = interpolate_template(selector, component)? {
170 let selector = crate::utils::parse_selector(&selector_string)?;
171 result_selectors.push(selector);
172 }
173 }
174 if result_selectors.is_empty() {
175 return Ok(None);
176 }
177 let mut event_codes = event_codes.to_vec();
178 event_codes.insert(0, component.event_id);
179 Ok(Some(MetricConfig {
180 event_codes,
181 selectors: result_selectors,
182 metric_id: *metric_id,
183 metric_type: *metric_type,
184 upload_once: *upload_once,
185 project_id: *project_id,
186 }))
187 }
188}
189
190const MONIKER_INTERPOLATION: &str = "{MONIKER}";
191const INSTANCE_ID_INTERPOLATION: &str = "{INSTANCE_ID}";
192
193fn interpolate_template(
196 template: &str,
197 component_info: &ComponentIdInfo,
198) -> Result<Option<String>, anyhow::Error> {
199 let moniker_position = template.find(MONIKER_INTERPOLATION);
200 let instance_id_position = template.find(INSTANCE_ID_INTERPOLATION);
201 let separator_position = template.find(":");
202 match (moniker_position, separator_position, instance_id_position, &component_info.instance_id)
208 {
209 (Some(i), Some(s), _, _) if i < s => {
210 Ok(Some(template.replace(MONIKER_INTERPOLATION, &component_info.moniker.to_string())))
211 }
212 (Some(_), Some(_), _, _) => Ok(Some(template.replace(
213 MONIKER_INTERPOLATION,
214 &selectors::sanitize_string_for_selectors(&component_info.moniker.to_string()),
215 ))),
216 (_, _, Some(_), Some(id)) => {
217 Ok(Some(template.replace(INSTANCE_ID_INTERPOLATION, &id.to_string())))
218 }
219 (_, _, Some(_), None) => Ok(None),
220 (None, _, None, _) => {
221 bail!(
222 "{} and {} not found in selector template {}",
223 MONIKER_INTERPOLATION,
224 INSTANCE_ID_INTERPOLATION,
225 template
226 )
227 }
228 (Some(_), None, _, _) => {
229 bail!("Separator ':' not found in selector template {}", template)
230 }
231 }
232}
233
234fn moniker_deserialize<'de, D>(deserializer: D) -> Result<ExtendedMoniker, D::Error>
235where
236 D: Deserializer<'de>,
237{
238 let moniker_str = String::deserialize(deserializer)?;
239 ExtendedMoniker::parse_str(&moniker_str).map_err(de::Error::custom)
240}
241
242fn instance_id_deserialize<'de, D>(deserializer: D) -> Result<Option<InstanceId>, D::Error>
243where
244 D: Deserializer<'de>,
245{
246 match Option::<String>::deserialize(deserializer)? {
247 None => Ok(None),
248 Some(instance_id) => {
249 let instance_id = InstanceId::from_str(&instance_id).map_err(de::Error::custom)?;
250 Ok(Some(instance_id))
251 }
252 }
253}
254
255pub fn moniker_serialize<S>(moniker: &ExtendedMoniker, serializer: S) -> Result<S::Ok, S::Error>
256where
257 S: Serializer,
258{
259 serializer.serialize_str(&moniker.to_string())
260}
261
262pub fn instance_id_serialize<S>(
263 instance_id: &Option<InstanceId>,
264 serializer: S,
265) -> Result<S::Ok, S::Error>
266where
267 S: Serializer,
268{
269 match instance_id.as_ref() {
270 Some(instance_id) => serializer.serialize_some(&instance_id.to_string()),
271 None => serializer.serialize_none(),
272 }
273}
274
275fn one_or_many_strings<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
276where
277 D: Deserializer<'de>,
278{
279 deserializer.deserialize_any(OneOrMany(PhantomData::<String>))
280}
281
282#[derive(Serialize, Deserialize, Default)]
283pub struct MergedSamplerConfig {
284 pub fire_project_templates: Vec<ProjectTemplate>,
285 pub fire_component_configs: Vec<ComponentIdInfoList>,
286 pub project_configs: Vec<ProjectConfig>,
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use moniker::Moniker;
293 use selectors::FastError;
294 use std::str::FromStr;
295
296 #[fuchsia::test]
297 fn parse_project_template() {
298 let template = r#"{
299 "project_id": 13,
300 "poll_rate_sec": 60,
301 "customer_id": 8,
302 "metrics": [
303 {
304 "selector": [
305 "{MONIKER}:root/path2:leaf2",
306 "foo/bar:root/{MONIKER}:leaf3",
307 "asdf/qwer:root/path4:pre-{MONIKER}-post",
308 ],
309 "metric_id": 2,
310 "metric_type": "Occurrence",
311 }
312 ]
313 }"#;
314 let config: ProjectTemplate = serde_json5::from_str(template).expect("deserialize");
315 assert_eq!(
316 config,
317 ProjectTemplate {
318 project_id: ProjectId(13),
319 poll_rate_sec: 60,
320 customer_id: CustomerId(8),
321 metrics: vec![MetricTemplate {
322 selectors: vec![
323 "{MONIKER}:root/path2:leaf2".into(),
324 "foo/bar:root/{MONIKER}:leaf3".into(),
325 "asdf/qwer:root/path4:pre-{MONIKER}-post".into(),
326 ],
327 event_codes: vec![],
328 project_id: None,
329 upload_once: false,
330 metric_id: MetricId(2),
331 metric_type: MetricType::Occurrence,
332 }],
333 }
334 );
335 }
336
337 #[fuchsia::test]
338 fn parse_component_config() {
339 let components_json = r#"[
340 {
341 "id": 42,
342 "label": "Foo_42",
343 "moniker": "core/foo42"
344 },
345 {
346 "id": 10,
347 "label": "Hello",
348 "instance_id": "8775ff0afe12ca578135014a5d36a7733b0f9982bcb62a888b007cb2c31a7046",
349 "moniker": "bootstrap/hello"
350 }
351 ]"#;
352 let config: ComponentIdInfoList =
353 serde_json5::from_str(components_json).expect("deserialize");
354 assert_eq!(
355 config,
356 ComponentIdInfoList(vec![
357 ComponentIdInfo {
358 moniker: ExtendedMoniker::parse_str("core/foo42").unwrap(),
359 instance_id: None,
360 event_id: EventCode(42),
361 label: "Foo_42".into()
362 },
363 ComponentIdInfo {
364 moniker: ExtendedMoniker::parse_str("bootstrap/hello").unwrap(),
365 instance_id: Some(
366 InstanceId::from_str(
367 "8775ff0afe12ca578135014a5d36a7733b0f9982bcb62a888b007cb2c31a7046"
368 )
369 .unwrap()
370 ),
371 event_id: EventCode(10),
372 label: "Hello".into()
373 },
374 ])
375 );
376 }
377
378 #[fuchsia::test]
379 fn template_expansion_basic() {
380 let project_template = ProjectTemplate {
381 project_id: ProjectId(13),
382 customer_id: CustomerId(7),
383 poll_rate_sec: 60,
384 metrics: vec![MetricTemplate {
385 selectors: vec!["{MONIKER}:root/path:leaf".into()],
386 metric_id: MetricId(1),
387 metric_type: MetricType::Occurrence,
388 event_codes: vec![EventCode(1), EventCode(2)],
389 project_id: None,
390 upload_once: true,
391 }],
392 };
393 let component_info = ComponentIdInfoList(vec![
394 ComponentIdInfo {
395 moniker: ExtendedMoniker::parse_str("core/foo42").unwrap(),
396 instance_id: None,
397 event_id: EventCode(42),
398 label: "Foo_42".into(),
399 },
400 ComponentIdInfo {
401 moniker: ExtendedMoniker::parse_str("bootstrap/hello").unwrap(),
402 instance_id: Some(
403 InstanceId::from_str(
404 "8775ff0afe12ca578135014a5d36a7733b0f9982bcb62a888b007cb2c31a7046",
405 )
406 .unwrap(),
407 ),
408 event_id: EventCode(43),
409 label: "Hello".into(),
410 },
411 ]);
412 let config = project_template.expand(&component_info).expect("expanded template");
413 assert_eq!(
414 config,
415 ProjectConfig {
416 project_id: ProjectId(13),
417 customer_id: CustomerId(7),
418 poll_rate_sec: 60,
419 metrics: vec![
420 MetricConfig {
421 selectors: vec![
422 selectors::parse_selector::<FastError>("core/foo42:root/path:leaf")
423 .unwrap()
424 ],
425 metric_id: MetricId(1),
426 metric_type: MetricType::Occurrence,
427 event_codes: vec![EventCode(42), EventCode(1), EventCode(2)],
428 upload_once: true,
429 project_id: None,
430 },
431 MetricConfig {
432 selectors: vec![
433 selectors::parse_selector::<FastError>(
434 "bootstrap/hello:root/path:leaf"
435 )
436 .unwrap()
437 ],
438 metric_id: MetricId(1),
439 metric_type: MetricType::Occurrence,
440 event_codes: vec![EventCode(43), EventCode(1), EventCode(2)],
441 upload_once: true,
442 project_id: None,
443 },
444 ],
445 }
446 );
447 }
448
449 #[fuchsia::test]
450 fn template_expansion_many_selectors() {
451 let project_template = ProjectTemplate {
452 project_id: ProjectId(13),
453 poll_rate_sec: 60,
454 customer_id: CustomerId(7),
455 metrics: vec![MetricTemplate {
456 selectors: vec![
457 "{MONIKER}:root/path2:leaf2".into(),
458 "foo/bar:root/{MONIKER}:leaf3".into(),
459 "asdf/qwer:root/path4:pre-{MONIKER}-post".into(),
460 ],
461 metric_id: MetricId(2),
462 metric_type: MetricType::Occurrence,
463 event_codes: vec![],
464 project_id: None,
465 upload_once: false,
466 }],
467 };
468 let component_info = ComponentIdInfoList(vec![
469 ComponentIdInfo {
470 moniker: ExtendedMoniker::parse_str("core/foo42").unwrap(),
471 instance_id: None,
472 event_id: EventCode(42),
473 label: "Foo_42".into(),
474 },
475 ComponentIdInfo {
476 moniker: ExtendedMoniker::parse_str("bootstrap/hello").unwrap(),
477 instance_id: Some(
478 InstanceId::from_str(
479 "8775ff0afe12ca578135014a5d36a7733b0f9982bcb62a888b007cb2c31a7046",
480 )
481 .unwrap(),
482 ),
483 event_id: EventCode(43),
484 label: "Hello".into(),
485 },
486 ]);
487 let config = project_template.expand(&component_info).expect("expanded template");
488 assert_eq!(
489 config,
490 ProjectConfig {
491 project_id: ProjectId(13),
492 customer_id: CustomerId(7),
493 poll_rate_sec: 60,
494 metrics: vec![
495 MetricConfig {
496 selectors: vec![
497 selectors::parse_selector::<FastError>("core/foo42:root/path2:leaf2")
498 .unwrap(),
499 selectors::parse_selector::<FastError>(
500 "foo/bar:root/core\\/foo42:leaf3"
501 )
502 .unwrap(),
503 selectors::parse_selector::<FastError>(
504 "asdf/qwer:root/path4:pre-core\\/foo42-post"
505 )
506 .unwrap()
507 ],
508 metric_id: MetricId(2),
509 metric_type: MetricType::Occurrence,
510 event_codes: vec![EventCode(42)],
511 upload_once: false,
512 project_id: None,
513 },
514 MetricConfig {
515 selectors: vec![
516 selectors::parse_selector::<FastError>(
517 "bootstrap/hello:root/path2:leaf2"
518 )
519 .unwrap(),
520 selectors::parse_selector::<FastError>(
521 "foo/bar:root/bootstrap\\/hello:leaf3"
522 )
523 .unwrap(),
524 selectors::parse_selector::<FastError>(
525 "asdf/qwer:root/path4:pre-bootstrap\\/hello-post"
526 )
527 .unwrap()
528 ],
529 metric_id: MetricId(2),
530 metric_type: MetricType::Occurrence,
531 event_codes: vec![EventCode(43)],
532 upload_once: false,
533 project_id: None,
534 },
535 ],
536 }
537 );
538 }
539
540 #[fuchsia::test]
541 fn index_substitution_works() {
542 let mut ids = component_id_index::Index::default();
543 let foo_bar_moniker = Moniker::parse_str("foo/bar").unwrap();
544 let qwer_asdf_moniker = Moniker::parse_str("qwer/asdf").unwrap();
545 ids.insert(
546 foo_bar_moniker,
547 "1234123412341234123412341234123412341234123412341234123412341234"
548 .parse::<InstanceId>()
549 .unwrap(),
550 )
551 .unwrap();
552 ids.insert(
553 qwer_asdf_moniker,
554 "1234abcd1234abcd123412341234123412341234123412341234123412341234"
555 .parse::<InstanceId>()
556 .unwrap(),
557 )
558 .unwrap();
559 let mut components = ComponentIdInfoList(vec![
560 ComponentIdInfo {
561 moniker: "baz/quux".try_into().unwrap(),
562 event_id: EventCode(101),
563 label: "bq".into(),
564 instance_id: None,
565 },
566 ComponentIdInfo {
567 moniker: "foo/bar".try_into().unwrap(),
568 event_id: EventCode(102),
569 label: "fb".into(),
570 instance_id: None,
571 },
572 ]);
573 components.add_instance_ids(&ids);
574 let moniker_template = "fizz/buzz:root/info/{MONIKER}/data";
575 let id_template = "fizz/buzz:root/info/{INSTANCE_ID}/data";
576 assert_eq!(
577 interpolate_template(moniker_template, &components[0]).unwrap().unwrap(),
578 "fizz/buzz:root/info/baz\\/quux/data".to_string()
579 );
580 assert_eq!(
581 interpolate_template(moniker_template, &components[1]).unwrap().unwrap(),
582 "fizz/buzz:root/info/foo\\/bar/data".to_string()
583 );
584 assert_eq!(interpolate_template(id_template, &components[0]).unwrap(), None);
585 assert_eq!(
586 interpolate_template(id_template, &components[1]).unwrap().unwrap(),
587 "fizz/buzz:root/info/1234123412341234123412341234123412341234123412341234123412341234/data"
588 );
589 }
590
591 #[derive(Debug, Deserialize, Eq, PartialEq)]
592 struct TestString(#[serde(deserialize_with = "super::one_or_many_strings")] Vec<String>);
593
594 #[fuchsia::test]
595 fn parse_valid_single_string() {
596 let json = "\"whatever-1982035*()$*H\"";
597 let data: TestString = serde_json5::from_str(json).expect("deserialize");
598 assert_eq!(data, TestString(vec!["whatever-1982035*()$*H".into()]));
599 }
600
601 #[fuchsia::test]
602 fn parse_valid_multiple_strings() {
603 let json = "[ \"core/foo:not:a:selector:root/branch:leaf\", \"core/bar:root/twig:leaf\"]";
604 let data: TestString = serde_json5::from_str(json).expect("deserialize");
605 assert_eq!(
606 data,
607 TestString(vec![
608 "core/foo:not:a:selector:root/branch:leaf".into(),
609 "core/bar:root/twig:leaf".into()
610 ])
611 );
612 }
613
614 #[fuchsia::test]
615 fn refuse_invalid_strings() {
616 let not_string = "42";
617 let bad_list = "[ 42, \"core/bar:not:a:selector:root/twig:leaf\"]";
618 serde_json5::from_str::<TestString>(not_string).expect_err("should fail");
619 serde_json5::from_str::<TestString>(bad_list).expect_err("should fail");
620 }
621}