Skip to main content

sampler_config/runtime/
mod.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 crate::common::{EventCode, MetricId, MetricType, ProjectId};
6use fidl_fuchsia_diagnostics::Selector;
7use selectors::SelectorDisplayOptions;
8use serde::ser::{Error, SerializeSeq};
9use serde::{Deserialize, Serialize, Serializer};
10
11/// Configuration for a single project to map Inspect data to its Cobalt metrics.
12#[derive(Serialize, Deserialize, Debug, PartialEq)]
13pub struct ProjectConfig {
14    /// Project ID that metrics are being sampled and forwarded on behalf of.
15    pub project_id: ProjectId,
16
17    /// Groupings of metrics that share a poll rate for this project.
18    pub data_sets: Vec<DataSetConfig>,
19}
20
21/// Grouping unit for metrics within a single project that share a polling
22/// frequency.
23#[derive(Serialize, Deserialize, Debug, PartialEq)]
24pub struct DataSetConfig {
25    /// The frequency with which metrics are sampled, in seconds.
26    #[serde(deserialize_with = "crate::utils::greater_than_zero")]
27    pub poll_rate_sec: i64,
28
29    /// The collection of mappings from Inspect to Cobalt.
30    pub metrics: Vec<MetricConfig>,
31}
32
33/// Configuration for a single metric to map from an Inspect property to a Cobalt metric.
34#[derive(Serialize, Deserialize, Debug, PartialEq)]
35pub struct MetricConfig {
36    /// Selector identifying the metric to sample via the diagnostics platform.
37    #[serde(
38        rename = "selector",
39        deserialize_with = "crate::utils::one_or_many_selectors",
40        serialize_with = "selectors_to_string"
41    )]
42    pub selectors: Vec<Selector>,
43
44    /// Cobalt metric id to map the selector to.
45    pub metric_id: MetricId,
46
47    /// Data type to transform the metric to.
48    pub metric_type: MetricType,
49
50    /// Event codes defining the dimensions of the Cobalt metric. Note: Order matters, and must
51    /// match the order of the defined dimensions in the Cobalt metric file.
52    /// Missing field means the same as empty list.
53    #[serde(default)]
54    pub event_codes: Vec<EventCode>,
55
56    /// Optional boolean specifying whether to upload the specified metric only once, the first time
57    /// it becomes available to the sampler. Defaults to false.
58    #[serde(default)]
59    pub upload_once: bool,
60}
61
62pub fn selectors_to_string<S>(selectors: &[Selector], serializer: S) -> Result<S::Ok, S::Error>
63where
64    S: Serializer,
65{
66    let mut seq = serializer.serialize_seq(Some(selectors.len()))?;
67    for selector in selectors {
68        let selector_string =
69            selectors::selector_to_string(selector, SelectorDisplayOptions::never_wrap_in_quotes())
70                .map_err(S::Error::custom)?;
71        seq.serialize_element(&selector_string)?;
72    }
73    seq.end()
74}
75
76#[cfg(test)]
77mod test {
78    use super::*;
79
80    #[fuchsia::test]
81    fn parse_valid_sampler_config() {
82        let ok_json = r#"{
83            "project_id": 5,
84            "data_sets": [{
85              "poll_rate_sec": 60,
86              "metrics": [
87                {
88                  "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
89                  "metric_id": 1,
90                  "metric_type": "Occurrence",
91                  "event_codes": [0, 0]
92                }
93              ]
94            }]
95        }"#;
96
97        let config: ProjectConfig = serde_json5::from_str(ok_json).expect("parse json");
98        assert_eq!(config, ProjectConfig {
99            project_id: ProjectId(5),
100            data_sets: vec![DataSetConfig {
101                poll_rate_sec: 60,
102                metrics: vec![
103                    MetricConfig {
104                        selectors: vec![
105                            selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
106                        ],
107                        metric_id: MetricId(1),
108                        metric_type: MetricType::Occurrence,
109                        event_codes: vec![EventCode(0), EventCode(0)],
110                        upload_once: false,
111                    }
112                ]
113            }]
114        });
115    }
116
117    #[fuchsia::test]
118    fn parse_valid_sampler_config_json5() {
119        let ok_json = r#"{
120          project_id: 5,
121          data_sets: [{
122            poll_rate_sec: 3,
123            metrics: [
124              {
125                // Test comment for json5 portability.
126                selector: "single_counter_test_component:root:counter",
127                metric_id: 1,
128                metric_type: "Occurrence",
129                event_codes: [0, 0]
130              }
131            ]
132          }]
133        }"#;
134
135        let config: ProjectConfig = serde_json5::from_str(ok_json).expect("parse json");
136        assert_eq!(
137            config,
138            ProjectConfig {
139                project_id: ProjectId(5),
140                data_sets: vec![DataSetConfig {
141                    poll_rate_sec: 3,
142                    metrics: vec![MetricConfig {
143                        selectors: vec![
144                            selectors::parse_verbose("single_counter_test_component:root:counter")
145                                .unwrap(),
146                        ],
147                        metric_id: MetricId(1),
148                        metric_type: MetricType::Occurrence,
149                        upload_once: false,
150                        event_codes: vec![EventCode(0), EventCode(0)],
151                    }]
152                }]
153            }
154        );
155    }
156
157    #[fuchsia::test]
158    fn parse_valid_sampler_config_multiple_selectors() {
159        let ok_json = r#"{
160          project_id: 5,
161          data_sets: [{
162            poll_rate_sec: 3,
163            metrics: [
164              {
165                selector: ["component:root:one", "component:root:two"],
166                metric_id: 1,
167                metric_type: "Occurrence",
168                event_codes: [0, 0]
169              }
170            ]
171          }]
172        }"#;
173
174        let config: ProjectConfig = serde_json5::from_str(ok_json).expect("parse json");
175        assert_eq!(
176            config,
177            ProjectConfig {
178                project_id: ProjectId(5),
179                data_sets: vec![DataSetConfig {
180                    poll_rate_sec: 3,
181                    metrics: vec![MetricConfig {
182                        selectors: vec![
183                            selectors::parse_verbose("component:root:one").unwrap(),
184                            selectors::parse_verbose("component:root:two").unwrap(),
185                        ],
186                        metric_id: MetricId(1),
187                        metric_type: MetricType::Occurrence,
188                        upload_once: false,
189                        event_codes: vec![EventCode(0), EventCode(0)],
190                    }]
191                }]
192            }
193        );
194    }
195
196    #[fuchsia::test]
197    fn parse_invalid_config() {
198        let invalid_json = r#"{
199          "project_id": 5,
200          "poll_rate_sec": 3,
201          "invalid_field": "bad bad bad"
202        }"#;
203
204        serde_json5::from_str::<ProjectConfig>(invalid_json)
205            .expect_err("fail to load invalid config");
206    }
207
208    #[fuchsia::test]
209    fn parse_optional_args() {
210        let true_json = r#"{
211           "project_id": 5,
212           "data_sets": [{
213             "poll_rate_sec": 60,
214             "metrics": [
215               {
216                 // Test comment for json5 portability.
217                 "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
218                 "metric_id": 1,
219                 "metric_type": "Occurrence",
220                 "event_codes": [0, 0],
221                 "upload_once": true,
222               }
223             ]
224           }]
225         }"#;
226
227        let config: ProjectConfig = serde_json5::from_str(true_json).expect("parse json");
228        assert_eq!(
229            config,
230            ProjectConfig {
231                project_id: ProjectId(5),
232                data_sets: vec![DataSetConfig {
233                    poll_rate_sec: 60,
234                    metrics: vec![MetricConfig {
235                        selectors: vec![
236                            selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
237                        ],
238                        metric_id: MetricId(1),
239                        metric_type: MetricType::Occurrence,
240                        upload_once: true,
241                        event_codes: vec![EventCode(0), EventCode(0)],
242                    }]
243                }]
244            }
245        );
246
247        let false_json = r#"{
248          "project_id": 5,
249          "data_sets": [{
250            "poll_rate_sec": 60,
251            "metrics": [
252              {
253                // Test comment for json5 portability.
254                "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
255                "metric_id": 1,
256                "metric_type": "Occurrence",
257                "event_codes": [0, 0],
258                "upload_once": false,
259              }
260            ]
261          }]
262        }"#;
263        let config: ProjectConfig = serde_json5::from_str(false_json).expect("parse json");
264        assert_eq!(
265            config,
266            ProjectConfig {
267                project_id: ProjectId(5),
268                data_sets: vec![DataSetConfig {
269                    poll_rate_sec: 60,
270                    metrics: vec![MetricConfig {
271                        selectors: vec![
272                            selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
273                        ],
274                        metric_id: MetricId(1),
275                        metric_type: MetricType::Occurrence,
276                        upload_once: false,
277                        event_codes: vec![EventCode(0), EventCode(0)],
278                    }]
279                }]
280            }
281        );
282    }
283    #[fuchsia::test]
284    fn default_customer_id() {
285        let default_json = r#"{
286          "project_id": 5,
287          "data_sets": [{
288            "poll_rate_sec": 60,
289            "metrics": [
290              {
291                "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
292                "metric_id": 1,
293                "metric_type": "Occurrence",
294                "event_codes": [0, 0]
295              }
296            ]
297          }]
298        }"#;
299
300        let config: ProjectConfig = serde_json5::from_str(default_json).expect("deserialize");
301        assert_eq!(
302            config,
303            ProjectConfig {
304                project_id: ProjectId(5),
305                data_sets: vec![DataSetConfig {
306                    poll_rate_sec: 60,
307                    metrics: vec![MetricConfig {
308                        selectors: vec![
309                            selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
310                        ],
311                        metric_id: MetricId(1),
312                        metric_type: MetricType::Occurrence,
313                        upload_once: false,
314                        event_codes: vec![EventCode(0), EventCode(0)],
315                    }],
316                }]
317            }
318        );
319    }
320
321    #[fuchsia::test]
322    fn missing_event_codes_ok() {
323        let default_json = r#"{
324          "project_id": 5,
325          "data_sets": [{
326            "poll_rate_sec": 60,
327            "metrics": [
328              {
329                "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
330                "metric_id": 1,
331                "metric_type": "Occurrence",
332              }
333            ]
334          }]
335        }"#;
336
337        let config: ProjectConfig = serde_json5::from_str(default_json).expect("deserialize");
338        assert_eq!(
339            config,
340            ProjectConfig {
341                project_id: ProjectId(5),
342                data_sets: vec![DataSetConfig {
343                    poll_rate_sec: 60,
344                    metrics: vec![MetricConfig {
345                        selectors: vec![
346                            selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
347                        ],
348                        metric_id: MetricId(1),
349                        metric_type: MetricType::Occurrence,
350                        upload_once: false,
351                        event_codes: vec![],
352                    }]
353                }]
354            }
355        );
356    }
357}