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(Clone, 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![selectors::parse_verbose(
142                        "single_counter_test_component:root:counter"
143                    )
144                    .unwrap(),],
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_invalid_config() {
157        let invalid_json = r#"{
158          "project_id": 5,
159          "poll_rate_sec": 3,
160          "invalid_field": "bad bad bad"
161        }"#;
162
163        serde_json5::from_str::<ProjectConfig>(invalid_json)
164            .expect_err("fail to load invalid config");
165    }
166
167    #[fuchsia::test]
168    fn parse_optional_args() {
169        let true_json = r#"{
170           "project_id": 5,
171           "poll_rate_sec": 60,
172           "metrics": [
173             {
174               // Test comment for json5 portability.
175               "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
176               "metric_id": 1,
177               "metric_type": "Occurrence",
178               "event_codes": [0, 0],
179               "upload_once": true,
180             }
181           ]
182         }"#;
183
184        let config: ProjectConfig = serde_json5::from_str(true_json).expect("parse json");
185        assert_eq!(
186            config,
187            ProjectConfig {
188                project_id: ProjectId(5),
189                poll_rate_sec: 60,
190                customer_id: CustomerId(1),
191                metrics: vec![MetricConfig {
192                    selectors: vec![
193                        selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
194                    ],
195                    metric_id: MetricId(1),
196                    metric_type: MetricType::Occurrence,
197                    upload_once: true,
198                    event_codes: vec![EventCode(0), EventCode(0)],
199                    project_id: None,
200                }]
201            }
202        );
203
204        let false_json = r#"{
205          "project_id": 5,
206          "poll_rate_sec": 60,
207          "metrics": [
208            {
209              // Test comment for json5 portability.
210              "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
211              "metric_id": 1,
212              "metric_type": "Occurrence",
213              "event_codes": [0, 0],
214              "upload_once": false,
215            }
216          ]
217        }"#;
218        let config: ProjectConfig = serde_json5::from_str(false_json).expect("parse json");
219        assert_eq!(
220            config,
221            ProjectConfig {
222                project_id: ProjectId(5),
223                poll_rate_sec: 60,
224                customer_id: CustomerId(1),
225                metrics: vec![MetricConfig {
226                    selectors: vec![
227                        selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
228                    ],
229                    metric_id: MetricId(1),
230                    metric_type: MetricType::Occurrence,
231                    upload_once: false,
232                    event_codes: vec![EventCode(0), EventCode(0)],
233                    project_id: None
234                }]
235            }
236        );
237    }
238    #[fuchsia::test]
239    fn default_customer_id() {
240        let default_json = r#"{
241          "project_id": 5,
242          "poll_rate_sec": 60,
243          "metrics": [
244            {
245              "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
246              "metric_id": 1,
247              "metric_type": "Occurrence",
248              "event_codes": [0, 0]
249            }
250          ]
251        }"#;
252        let with_customer_id_json = r#"{
253            "customer_id": 6,
254            "project_id": 5,
255            "poll_rate_sec": 3,
256            "metrics": [
257              {
258                "selector": "single_counter_test_component:root:counter",
259                "metric_id": 1,
260                "metric_type": "Occurrence",
261                "event_codes": [0, 0]
262              }
263            ]
264          }
265          "#;
266
267        let config: ProjectConfig = serde_json5::from_str(default_json).expect("deserialize");
268        assert_eq!(
269            config,
270            ProjectConfig {
271                project_id: ProjectId(5),
272                poll_rate_sec: 60,
273                customer_id: CustomerId(1),
274                metrics: vec![MetricConfig {
275                    selectors: vec![
276                        selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
277                    ],
278                    metric_id: MetricId(1),
279                    metric_type: MetricType::Occurrence,
280                    upload_once: false,
281                    event_codes: vec![EventCode(0), EventCode(0)],
282                    project_id: None,
283                }],
284            }
285        );
286
287        let config: ProjectConfig =
288            serde_json5::from_str(with_customer_id_json).expect("deserialize");
289        assert_eq!(
290            config,
291            ProjectConfig {
292                project_id: ProjectId(5),
293                poll_rate_sec: 3,
294                customer_id: CustomerId(6),
295                metrics: vec![MetricConfig {
296                    selectors: vec![selectors::parse_verbose(
297                        "single_counter_test_component:root:counter"
298                    )
299                    .unwrap(),],
300                    metric_id: MetricId(1),
301                    metric_type: MetricType::Occurrence,
302                    upload_once: false,
303                    event_codes: vec![EventCode(0), EventCode(0)],
304                    project_id: None,
305                }]
306            }
307        );
308    }
309
310    #[fuchsia::test]
311    fn missing_event_codes_ok() {
312        let default_json = r#"{
313          "project_id": 5,
314          "poll_rate_sec": 60,
315          "metrics": [
316            {
317              "selector": "bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests",
318              "metric_id": 1,
319              "metric_type": "Occurrence",
320            }
321          ]
322        }"#;
323
324        let config: ProjectConfig = serde_json5::from_str(default_json).expect("deserialize");
325        assert_eq!(
326            config,
327            ProjectConfig {
328                project_id: ProjectId(5),
329                poll_rate_sec: 60,
330                customer_id: CustomerId(1),
331                metrics: vec![MetricConfig {
332                    selectors: vec![
333                        selectors::parse_verbose("bootstrap/archivist:root/all_archive_accessor:inspect_batch_iterator_get_next_requests").unwrap(),
334                    ],
335                    metric_id: MetricId(1),
336                    metric_type: MetricType::Occurrence,
337                    upload_once: false,
338                    event_codes: vec![],
339                    project_id: None,
340                }]
341            }
342        );
343    }
344}