1use 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#[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}