Skip to main content

sampler_config/input/
mod.rs

1// Copyright 2026 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
5//! The format of the Sampler config files that are checked into the tree.
6//!
7//! These configs identify Cobalt metrics by ID or by name. `generate_sampler_configs` validates
8//! them against the Cobalt registry and resolves them into the [`crate::runtime`] and
9//! [`crate::assembly`] types, which identify metrics only by ID.
10
11use crate::common::{EventCode, MetricId, MetricType, ProjectId};
12use fidl_fuchsia_diagnostics_common::Selector;
13use serde::{Deserialize, Deserializer, de};
14use std::fmt;
15
16/// An event code in an input Sampler config, specified either as its numeric code or as its
17/// name in the Cobalt registry.
18#[derive(Debug, PartialEq)]
19pub enum EventCodeSpec {
20    /// A numeric event code.
21    Code(EventCode),
22    /// The name of an event code in the Cobalt registry.
23    Name(String),
24}
25
26impl<'de> Deserialize<'de> for EventCodeSpec {
27    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
28    where
29        D: Deserializer<'de>,
30    {
31        deserializer.deserialize_any(EventCodeSpecVisitor)
32    }
33}
34
35struct EventCodeSpecVisitor;
36
37impl<'de> de::Visitor<'de> for EventCodeSpecVisitor {
38    type Value = EventCodeSpec;
39
40    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.write_str("a u32 event code or an event code name string")
42    }
43
44    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
45        u32::try_from(value)
46            .map(|code| EventCodeSpec::Code(EventCode(code)))
47            .map_err(|_| E::custom(format!("event code {value} does not fit in a u32")))
48    }
49
50    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
51        u32::try_from(value)
52            .map(|code| EventCodeSpec::Code(EventCode(code)))
53            .map_err(|_| E::custom(format!("event code {value} does not fit in a u32")))
54    }
55
56    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
57        Ok(EventCodeSpec::Name(value.to_owned()))
58    }
59}
60
61/// Configuration for a single project to map Inspect data to its Cobalt metrics.
62#[derive(Deserialize, Debug, PartialEq)]
63pub struct ProjectConfig {
64    /// Project ID that metrics are being sampled and forwarded on behalf of.
65    pub project_id: ProjectId,
66
67    /// Groupings of metrics that share a poll rate for this project.
68    pub data_sets: Vec<DataSetConfig>,
69}
70
71/// Grouping unit for metrics within a single project that share a polling frequency.
72#[derive(Deserialize, Debug, PartialEq)]
73pub struct DataSetConfig {
74    /// The frequency with which metrics are sampled, in seconds.
75    #[serde(deserialize_with = "crate::utils::greater_than_zero")]
76    pub poll_rate_sec: i64,
77
78    /// The collection of mappings from Inspect to Cobalt.
79    pub metrics: Vec<MetricConfig>,
80}
81
82/// Configuration for a single metric to map from an Inspect property to a Cobalt metric.
83///
84/// Exactly one of `metric_id` and `metric_name` must be specified.
85#[derive(Deserialize, Debug, PartialEq)]
86pub struct MetricConfig {
87    /// Selector identifying the metric to sample via the diagnostics platform.
88    #[serde(rename = "selector", deserialize_with = "crate::utils::one_or_many_selectors")]
89    pub selectors: Vec<Selector>,
90
91    /// ID of the Cobalt metric to map the selector to.
92    #[serde(default)]
93    pub metric_id: Option<MetricId>,
94
95    /// Name of the Cobalt metric to map the selector to.
96    #[serde(default)]
97    pub metric_name: Option<String>,
98
99    /// Data type to transform the metric to.
100    pub metric_type: MetricType,
101
102    /// Event codes defining the dimensions of the Cobalt metric. Note: Order matters, and must
103    /// match the order of the defined dimensions in the Cobalt metric file.
104    /// Missing field means the same as empty list.
105    #[serde(default)]
106    pub event_codes: Vec<EventCodeSpec>,
107
108    /// Optional boolean specifying whether to upload the specified metric only once, the first time
109    /// it becomes available to the sampler. Defaults to false.
110    #[serde(default)]
111    pub upload_once: bool,
112}
113
114/// Template for a FIRE project, which is expanded into a [`crate::runtime::ProjectConfig`] for
115/// each of the FIRE components.
116#[derive(Deserialize, Debug, PartialEq)]
117pub struct ProjectTemplate {
118    /// Project ID that metrics are being sampled and forwarded on behalf of.
119    pub project_id: ProjectId,
120
121    /// The frequency with which metrics are sampled, in seconds.
122    #[serde(deserialize_with = "crate::utils::greater_than_zero")]
123    pub poll_rate_sec: i64,
124
125    /// The collection of mappings from Inspect to Cobalt.
126    pub metrics: Vec<MetricTemplate>,
127}
128
129/// Template for a single FIRE metric.
130///
131/// Exactly one of `metric_id` and `metric_name` must be specified.
132#[derive(Deserialize, Debug, PartialEq)]
133pub struct MetricTemplate {
134    /// Selector templates identifying the metric to sample via the diagnostics platform.
135    #[serde(rename = "selector", deserialize_with = "crate::utils::one_or_many_strings")]
136    pub selectors: Vec<String>,
137
138    /// ID of the Cobalt metric to map the selector to.
139    #[serde(default)]
140    pub metric_id: Option<MetricId>,
141
142    /// Name of the Cobalt metric to map the selector to.
143    #[serde(default)]
144    pub metric_name: Option<String>,
145
146    /// Data type to transform the metric to.
147    pub metric_type: MetricType,
148
149    /// Event codes defining the dimensions of the Cobalt metric, after the FIRE component ID
150    /// dimension. Missing field means the same as empty list.
151    #[serde(default)]
152    pub event_codes: Vec<EventCodeSpec>,
153
154    /// Optional boolean specifying whether to upload the specified metric only once, the first time
155    /// it becomes available to the sampler. Defaults to false.
156    #[serde(default)]
157    pub upload_once: bool,
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[fuchsia::test]
165    fn parse_project_config_with_metric_name() {
166        let json = r#"{
167            project_id: 10,
168            data_sets: [{
169                poll_rate_sec: 60,
170                metrics: [{
171                    selector: "core/foo:root:bar",
172                    metric_name: "test_occurrence",
173                    metric_type: "Occurrence",
174                    event_codes: [1],
175                }],
176            }],
177        }"#;
178        let config: ProjectConfig = serde_json5::from_str(json).expect("parse json");
179        assert_eq!(
180            config,
181            ProjectConfig {
182                project_id: ProjectId(10),
183                data_sets: vec![DataSetConfig {
184                    poll_rate_sec: 60,
185                    metrics: vec![MetricConfig {
186                        selectors: vec![selectors::parse_verbose("core/foo:root:bar").unwrap()],
187                        metric_id: None,
188                        metric_name: Some("test_occurrence".into()),
189                        metric_type: MetricType::Occurrence,
190                        event_codes: vec![EventCodeSpec::Code(EventCode(1))],
191                        upload_once: false,
192                    }],
193                }],
194            }
195        );
196    }
197
198    #[fuchsia::test]
199    fn parse_project_template_with_metric_name() {
200        let json = r#"{
201            project_id: 10,
202            poll_rate_sec: 60,
203            metrics: [{
204                selector: "{MONIKER}:root:val",
205                metric_name: "test_fire_histogram",
206                metric_type: "IntHistogram",
207                event_codes: [2],
208            }],
209        }"#;
210        let template: ProjectTemplate = serde_json5::from_str(json).expect("parse json");
211        assert_eq!(
212            template,
213            ProjectTemplate {
214                project_id: ProjectId(10),
215                poll_rate_sec: 60,
216                metrics: vec![MetricTemplate {
217                    selectors: vec!["{MONIKER}:root:val".into()],
218                    metric_id: None,
219                    metric_name: Some("test_fire_histogram".into()),
220                    metric_type: MetricType::IntHistogram,
221                    event_codes: vec![EventCodeSpec::Code(EventCode(2))],
222                    upload_once: false,
223                }],
224            }
225        );
226    }
227
228    #[fuchsia::test]
229    fn parse_event_codes_with_names() {
230        let json = r#"{
231            selector: "{MONIKER}:root:val",
232            metric_name: "test_fire_histogram",
233            metric_type: "IntHistogram",
234            event_codes: [1, "Failed", "3"],
235        }"#;
236        let template: MetricTemplate = serde_json5::from_str(json).expect("parse json");
237        assert_eq!(
238            template.event_codes,
239            vec![
240                EventCodeSpec::Code(EventCode(1)),
241                EventCodeSpec::Name("Failed".into()),
242                EventCodeSpec::Name("3".into()),
243            ]
244        );
245    }
246
247    #[fuchsia::test]
248    fn parse_invalid_event_codes() {
249        for (event_code, expected_error) in [
250            ("-1", "event code -1 does not fit in a u32"),
251            ("4294967296", "event code 4294967296 does not fit in a u32"),
252            ("3.5", "expected a u32 event code or an event code name string"),
253            ("true", "expected a u32 event code or an event code name string"),
254        ] {
255            let json = format!(
256                r#"{{
257                    selector: "{{MONIKER}}:root:val",
258                    metric_name: "test_fire_histogram",
259                    metric_type: "IntHistogram",
260                    event_codes: [{event_code}],
261                }}"#
262            );
263            let err = serde_json5::from_str::<MetricTemplate>(&json).unwrap_err();
264            assert!(
265                err.to_string().contains(expected_error),
266                "event code {event_code}: unexpected error: {err}"
267            );
268        }
269    }
270}