Skip to main content

sampler/
config.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Error;
6use fidl_fuchsia_diagnostics as fdiagnostics;
7use fuchsia_inspect::{Node, NumericProperty, UintProperty};
8use fuchsia_inspect_contrib::nodes::BoundedListNode;
9use sampler_component_config::Config as ComponentConfig;
10use sampler_config::runtime::ProjectConfig;
11use sampler_config::{MetricType, ProjectId};
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::iter::Sum;
15
16/// Container for all configurations needed to instantiate the Sampler infrastructure.
17/// Includes:
18///      - Project configurations.
19///      - Whether to configure the ArchiveReader for tests (e.g. longer timeouts)
20///      - Minimum sample rate.
21#[derive(Debug)]
22pub struct SamplerConfig {
23    pub project_configs: Vec<ProjectConfig>,
24    pub stats: SamplerStats,
25}
26
27#[derive(Debug)]
28pub struct ProjectStats {
29    _project_node: Node,
30    pub metrics_configured: UintProperty,
31    pub cobalt_logs_sent: UintProperty,
32    pub events: RefCell<BoundedListNode>,
33}
34
35#[derive(Default, Debug)]
36pub struct SamplerStats {
37    pub projects: HashMap<ProjectId, ProjectStats>,
38}
39
40fn flatten_configs(mut input: Vec<ProjectConfig>) -> Vec<ProjectConfig> {
41    input.sort_unstable_by_key(|conf| *conf.project_id);
42    input.dedup_by(|next, prev| {
43        if next.project_id == prev.project_id {
44            prev.data_sets.append(&mut next.data_sets);
45            return true;
46        }
47
48        false
49    });
50
51    input
52}
53
54impl SamplerConfig {
55    pub fn new(config: ComponentConfig, stats: &Node) -> Result<Self, Error> {
56        let ComponentConfig { minimum_sample_rate_sec, project_configs } = config;
57        let mut sampler_stats = SamplerStats::default();
58        let project_configs = project_configs
59            .into_iter()
60            .map(|config| {
61                let config: ProjectConfig = serde_json::from_str(&config)?;
62                for ds in &config.data_sets {
63                    if ds.poll_rate_sec < minimum_sample_rate_sec {
64                        return Err(anyhow::anyhow!(
65                            "Data set in project {} had illegal poll rate. Actual: {}s, Min: {}s",
66                            config.project_id,
67                            ds.poll_rate_sec,
68                            minimum_sample_rate_sec
69                        ));
70                    }
71                }
72
73                Ok(config)
74            })
75            .collect::<Result<Vec<_>, Error>>()?;
76        let project_configs = flatten_configs(project_configs);
77        for config in &project_configs {
78            sampler_stats
79                .projects
80                .entry(config.project_id)
81                .and_modify(|project| {
82                    project
83                        .metrics_configured
84                        .add(u64::sum(config.data_sets.iter().map(|ds| ds.metrics.len() as u64)));
85                })
86                .or_insert_with(|| {
87                    let project_node = stats.create_child(format!("project_{}", config.project_id));
88                    let metrics_configured = project_node.create_uint(
89                        "metrics_configured",
90                        u64::sum(config.data_sets.iter().map(|ds| ds.metrics.len() as u64)),
91                    );
92                    let cobalt_logs_sent = project_node.create_uint("cobalt_logs_sent", 0);
93                    let events = RefCell::new(BoundedListNode::new(
94                        project_node.create_child("events"),
95                        300,
96                    ));
97                    ProjectStats {
98                        _project_node: project_node,
99                        metrics_configured,
100                        cobalt_logs_sent,
101                        events,
102                    }
103                });
104        }
105
106        Ok(Self { project_configs, stats: sampler_stats })
107    }
108
109    pub fn sample_data(&self) -> Vec<fdiagnostics::SampleDatum> {
110        let mut data = vec![];
111        for project in &self.project_configs {
112            for data_set in &project.data_sets {
113                for metric in &data_set.metrics {
114                    let strategy = Some(match metric.metric_type {
115                        MetricType::Integer | MetricType::String => {
116                            fdiagnostics::SampleStrategy::Always
117                        }
118                        MetricType::IntHistogram | MetricType::Occurrence => {
119                            fdiagnostics::SampleStrategy::OnDiff
120                        }
121                    });
122
123                    for selector in &metric.selectors {
124                        data.push(fdiagnostics::SampleDatum {
125                            selector: Some(fdiagnostics::SelectorArgument::StructuredSelector(
126                                selector.clone(),
127                            )),
128                            interval_secs: Some(data_set.poll_rate_sec),
129                            strategy,
130                            ..Default::default()
131                        });
132                    }
133                }
134            }
135        }
136
137        data
138    }
139}
140
141#[cfg(test)]
142mod test {
143    use super::*;
144    use diagnostics_assertions::assert_json_diff;
145    use fuchsia_inspect::*;
146    use sampler_config::runtime::*;
147    use sampler_config::*;
148    use selectors::parse_verbose;
149
150    const TEST_CONFIG_P5_1: &str =
151        include_str!("../testing/realm-factory/configs/test_config.json");
152    const TEST_CONFIG_P5_2: &str =
153        include_str!("../testing/realm-factory/configs/reboot_required_config.json");
154    const TEST_CONFIG_P6: &str =
155        include_str!("../testing/realm-factory/configs/test_config_2.json");
156
157    #[fuchsia::test]
158    async fn single_config() {
159        let inspector = Inspector::default();
160        let sc = SamplerConfig::new(
161            ComponentConfig {
162                minimum_sample_rate_sec: 1,
163                project_configs: vec![TEST_CONFIG_P5_1.to_string()],
164            },
165            inspector.root(),
166        )
167        .unwrap();
168
169        let SamplerConfig { project_configs, stats: _ } = sc;
170
171        assert_json_diff!(inspector, root: {
172            project_5: {
173                cobalt_logs_sent: 0,
174                metrics_configured: 3,
175                events: {},
176            }
177        });
178
179        assert_eq!(project_configs.len(), 1);
180        assert_eq!(
181            project_configs[0],
182            ProjectConfig {
183                project_id: ProjectId(5),
184                data_sets: vec![DataSetConfig {
185                    poll_rate_sec: 3,
186                    metrics: vec![
187                        MetricConfig {
188                            selectors: vec![
189                                parse_verbose("single_counter:root/samples:counter").unwrap(),
190                            ],
191                            metric_id: MetricId(101),
192                            metric_type: MetricType::Occurrence,
193                            event_codes: vec![EventCode(0), EventCode(0)],
194                            upload_once: false,
195                        },
196                        MetricConfig {
197                            selectors: vec![
198                                parse_verbose("single_counter:root/samples:integer_1").unwrap(),
199                            ],
200                            metric_id: MetricId(102),
201                            metric_type: MetricType::Integer,
202                            event_codes: vec![EventCode(0), EventCode(0)],
203                            upload_once: false,
204                        },
205                        MetricConfig {
206                            selectors: vec![
207                                parse_verbose("single_counter:root/samples:integer_2").unwrap(),
208                            ],
209                            metric_id: MetricId(103),
210                            metric_type: MetricType::Integer,
211                            event_codes: vec![EventCode(0), EventCode(0)],
212                            upload_once: true,
213                        },
214                    ],
215                }]
216            }
217        );
218    }
219
220    #[fuchsia::test]
221    fn error_on_invalid_sample_rate() {
222        let inspector = Inspector::default();
223        assert!(
224            SamplerConfig::new(
225                ComponentConfig {
226                    minimum_sample_rate_sec: 1000,
227                    project_configs: vec![TEST_CONFIG_P5_1.to_string()],
228                },
229                inspector.root(),
230            )
231            .is_err()
232        );
233    }
234
235    #[fuchsia::test]
236    async fn duped_project_id() {
237        let inspector = Inspector::default();
238        let sc = SamplerConfig::new(
239            ComponentConfig {
240                minimum_sample_rate_sec: 1,
241                project_configs: vec![TEST_CONFIG_P5_1.to_string(), TEST_CONFIG_P5_2.to_string()],
242            },
243            inspector.root(),
244        )
245        .unwrap();
246
247        let SamplerConfig { project_configs, stats: _ } = sc;
248
249        assert_json_diff!(inspector, root: {
250            project_5: {
251                cobalt_logs_sent: 0,
252                metrics_configured: 4,
253                events: {},
254            }
255        });
256
257        assert_eq!(project_configs.len(), 1);
258        assert_eq!(
259            project_configs[0],
260            ProjectConfig {
261                project_id: ProjectId(5),
262                data_sets: vec![
263                    DataSetConfig {
264                        poll_rate_sec: 3,
265                        metrics: vec![
266                            MetricConfig {
267                                selectors: vec![
268                                    parse_verbose("single_counter:root/samples:counter").unwrap(),
269                                ],
270                                metric_id: MetricId(101),
271                                metric_type: MetricType::Occurrence,
272                                event_codes: vec![EventCode(0), EventCode(0)],
273                                upload_once: false,
274                            },
275                            MetricConfig {
276                                selectors: vec![
277                                    parse_verbose("single_counter:root/samples:integer_1").unwrap(),
278                                ],
279                                metric_id: MetricId(102),
280                                metric_type: MetricType::Integer,
281                                event_codes: vec![EventCode(0), EventCode(0)],
282                                upload_once: false,
283                            },
284                            MetricConfig {
285                                selectors: vec![
286                                    parse_verbose("single_counter:root/samples:integer_2").unwrap(),
287                                ],
288                                metric_id: MetricId(103),
289                                metric_type: MetricType::Integer,
290                                event_codes: vec![EventCode(0), EventCode(0)],
291                                upload_once: true,
292                            },
293                        ],
294                    },
295                    DataSetConfig {
296                        poll_rate_sec: 3000,
297                        metrics: vec![MetricConfig {
298                            selectors: vec![
299                                parse_verbose("single_counter:root/samples:counter").unwrap(),
300                            ],
301                            metric_id: MetricId(104),
302                            metric_type: MetricType::Occurrence,
303                            event_codes: vec![EventCode(0), EventCode(0)],
304                            upload_once: false,
305                        },],
306                    },
307                ]
308            }
309        );
310    }
311
312    #[fuchsia::test]
313    async fn multi_project() {
314        let inspector = Inspector::default();
315        let sc = SamplerConfig::new(
316            ComponentConfig {
317                minimum_sample_rate_sec: 1,
318                project_configs: vec![
319                    TEST_CONFIG_P5_1.to_string(),
320                    TEST_CONFIG_P5_2.to_string(),
321                    TEST_CONFIG_P6.to_string(),
322                ],
323            },
324            inspector.root(),
325        )
326        .unwrap();
327
328        let SamplerConfig { project_configs, stats: _ } = sc;
329
330        assert_json_diff!(inspector, root: {
331            project_5: {
332                cobalt_logs_sent: 0,
333                metrics_configured: 4,
334                events: {},
335            },
336            project_6: {
337                cobalt_logs_sent: 0,
338                metrics_configured: 1,
339                events: {},
340            }
341        });
342
343        assert_eq!(project_configs.len(), 2);
344        assert_eq!(
345            project_configs[0],
346            ProjectConfig {
347                project_id: ProjectId(5),
348                data_sets: vec![
349                    DataSetConfig {
350                        poll_rate_sec: 3,
351                        metrics: vec![
352                            MetricConfig {
353                                selectors: vec![
354                                    parse_verbose("single_counter:root/samples:counter").unwrap(),
355                                ],
356                                metric_id: MetricId(101),
357                                metric_type: MetricType::Occurrence,
358                                event_codes: vec![EventCode(0), EventCode(0)],
359                                upload_once: false,
360                            },
361                            MetricConfig {
362                                selectors: vec![
363                                    parse_verbose("single_counter:root/samples:integer_1").unwrap(),
364                                ],
365                                metric_id: MetricId(102),
366                                metric_type: MetricType::Integer,
367                                event_codes: vec![EventCode(0), EventCode(0)],
368                                upload_once: false,
369                            },
370                            MetricConfig {
371                                selectors: vec![
372                                    parse_verbose("single_counter:root/samples:integer_2").unwrap(),
373                                ],
374                                metric_id: MetricId(103),
375                                metric_type: MetricType::Integer,
376                                event_codes: vec![EventCode(0), EventCode(0)],
377                                upload_once: true,
378                            },
379                        ],
380                    },
381                    DataSetConfig {
382                        poll_rate_sec: 3000,
383                        metrics: vec![MetricConfig {
384                            selectors: vec![
385                                parse_verbose("single_counter:root/samples:counter").unwrap(),
386                            ],
387                            metric_id: MetricId(104),
388                            metric_type: MetricType::Occurrence,
389                            event_codes: vec![EventCode(0), EventCode(0)],
390                            upload_once: false,
391                        },],
392                    },
393                ]
394            }
395        );
396        assert_eq!(
397            project_configs[1],
398            ProjectConfig {
399                project_id: ProjectId(6),
400                data_sets: vec![DataSetConfig {
401                    poll_rate_sec: 10,
402                    metrics: vec![MetricConfig {
403                        selectors: vec![parse_verbose("foo:bar:baz").unwrap(),],
404                        metric_id: MetricId(101),
405                        metric_type: MetricType::IntHistogram,
406                        event_codes: vec![EventCode(0)],
407                        upload_once: false,
408                    },],
409                },],
410            },
411        );
412    }
413}