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