1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! The different metric types supported by the Glean SDK to handle data.

use std::collections::HashMap;
use std::sync::atomic::Ordering;

use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use serde_json::json;
pub use serde_json::Value as JsonValue;

mod boolean;
mod counter;
mod custom_distribution;
mod datetime;
mod denominator;
mod event;
mod experiment;
pub(crate) mod labeled;
mod memory_distribution;
mod memory_unit;
mod numerator;
mod object;
mod ping;
mod quantity;
mod rate;
mod recorded_experiment;
mod remote_settings_config;
mod string;
mod string_list;
mod text;
mod time_unit;
mod timespan;
mod timing_distribution;
mod url;
mod uuid;

use crate::common_metric_data::CommonMetricDataInternal;
pub use crate::event_database::RecordedEvent;
use crate::histogram::{Functional, Histogram, PrecomputedExponential, PrecomputedLinear};
pub use crate::metrics::datetime::Datetime;
use crate::util::get_iso_time_string;
use crate::Glean;

pub use self::boolean::BooleanMetric;
pub use self::counter::CounterMetric;
pub use self::custom_distribution::CustomDistributionMetric;
pub use self::datetime::DatetimeMetric;
pub use self::denominator::DenominatorMetric;
pub use self::event::EventMetric;
pub(crate) use self::experiment::ExperimentMetric;
pub use self::labeled::{LabeledBoolean, LabeledCounter, LabeledMetric, LabeledString};
pub use self::memory_distribution::MemoryDistributionMetric;
pub use self::memory_unit::MemoryUnit;
pub use self::numerator::NumeratorMetric;
pub use self::object::ObjectMetric;
pub use self::ping::PingType;
pub use self::quantity::QuantityMetric;
pub use self::rate::{Rate, RateMetric};
pub use self::string::StringMetric;
pub use self::string_list::StringListMetric;
pub use self::text::TextMetric;
pub use self::time_unit::TimeUnit;
pub use self::timespan::TimespanMetric;
pub use self::timing_distribution::TimerId;
pub use self::timing_distribution::TimingDistributionMetric;
pub use self::url::UrlMetric;
pub use self::uuid::UuidMetric;
pub use crate::histogram::HistogramType;
pub use recorded_experiment::RecordedExperiment;

pub use self::remote_settings_config::RemoteSettingsConfig;

/// A snapshot of all buckets and the accumulated sum of a distribution.
//
// Note: Be careful when changing this structure.
// The serialized form ends up in the ping payload.
// New fields might require to be skipped on serialization.
#[derive(Debug, Serialize)]
pub struct DistributionData {
    /// A map containig the bucket index mapped to the accumulated count.
    ///
    /// This can contain buckets with a count of `0`.
    pub values: HashMap<i64, i64>,

    /// The accumulated sum of all the samples in the distribution.
    pub sum: i64,

    /// The total number of entries in the distribution.
    #[serde(skip)]
    pub count: i64,
}

/// The available metrics.
///
/// This is the in-memory and persisted layout of a metric.
///
/// ## Note
///
/// The order of metrics in this enum is important, as it is used for serialization.
/// Do not reorder the variants.
///
/// **Any new metric must be added at the end.**
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum Metric {
    /// A boolean metric. See [`BooleanMetric`] for more information.
    Boolean(bool),
    /// A counter metric. See [`CounterMetric`] for more information.
    Counter(i32),
    /// A custom distribution with precomputed exponential bucketing.
    /// See [`CustomDistributionMetric`] for more information.
    CustomDistributionExponential(Histogram<PrecomputedExponential>),
    /// A custom distribution with precomputed linear bucketing.
    /// See [`CustomDistributionMetric`] for more information.
    CustomDistributionLinear(Histogram<PrecomputedLinear>),
    /// A datetime metric. See [`DatetimeMetric`] for more information.
    Datetime(DateTime<FixedOffset>, TimeUnit),
    /// An experiment metric. See `ExperimentMetric` for more information.
    Experiment(recorded_experiment::RecordedExperiment),
    /// A quantity metric. See [`QuantityMetric`] for more information.
    Quantity(i64),
    /// A string metric. See [`StringMetric`] for more information.
    String(String),
    /// A string list metric. See [`StringListMetric`] for more information.
    StringList(Vec<String>),
    /// A UUID metric. See [`UuidMetric`] for more information.
    Uuid(String),
    /// A timespan metric. See [`TimespanMetric`] for more information.
    Timespan(std::time::Duration, TimeUnit),
    /// A timing distribution. See [`TimingDistributionMetric`] for more information.
    TimingDistribution(Histogram<Functional>),
    /// A memory distribution. See [`MemoryDistributionMetric`] for more information.
    MemoryDistribution(Histogram<Functional>),
    /// **DEPRECATED**: A JWE metric..
    /// Note: This variant MUST NOT be removed to avoid backwards-incompatible changes to the
    /// serialization. This type has no underlying implementation anymore.
    Jwe(String),
    /// A rate metric. See [`RateMetric`] for more information.
    Rate(i32, i32),
    /// A URL metric. See [`UrlMetric`] for more information.
    Url(String),
    /// A Text metric. See [`TextMetric`] for more information.
    Text(String),
    /// An Object metric. See [`ObjectMetric`] for more information.
    Object(String),
}

/// A [`MetricType`] describes common behavior across all metrics.
pub trait MetricType {
    /// Access the stored metadata
    fn meta(&self) -> &CommonMetricDataInternal;

    /// Create a new metric from this with a new name.
    fn with_name(&self, _name: String) -> Self
    where
        Self: Sized,
    {
        unimplemented!()
    }

    /// Create a new metric from this with a specific label.
    fn with_dynamic_label(&self, _label: String) -> Self
    where
        Self: Sized,
    {
        unimplemented!()
    }

    /// Whether this metric should currently be recorded
    ///
    /// This depends on the metrics own state, as determined by its metadata,
    /// and whether upload is enabled on the Glean object.
    fn should_record(&self, glean: &Glean) -> bool {
        if !glean.is_upload_enabled() {
            return false;
        }

        // Technically nothing prevents multiple calls to should_record() to run in parallel,
        // meaning both are reading self.meta().disabled and later writing it. In between it can
        // also read remote_settings_config, which also could be modified in between those 2 reads.
        // This means we could write the wrong remote_settings_epoch | current_disabled value. All in all
        // at worst we would see that metric enabled/disabled wrongly once.
        // But since everything is tunneled through the dispatcher, this should never ever happen.

        // Get the current disabled field from the metric metadata, including
        // the encoded remote_settings epoch
        let disabled_field = self.meta().disabled.load(Ordering::Relaxed);
        // Grab the epoch from the upper nibble
        let epoch = disabled_field >> 4;
        // Get the disabled flag from the lower nibble
        let disabled = disabled_field & 0xF;
        // Get the current remote_settings epoch to see if we need to bother with the
        // more expensive HashMap lookup
        let remote_settings_epoch = glean.remote_settings_epoch.load(Ordering::Acquire);
        if epoch == remote_settings_epoch {
            return disabled == 0;
        }
        // The epoch's didn't match so we need to look up the disabled flag
        // by the base_identifier from the in-memory HashMap
        let remote_settings_config = &glean.remote_settings_config.lock().unwrap();
        // Get the value from the remote configuration if it is there, otherwise return the default value.
        let current_disabled = {
            let base_id = self.meta().base_identifier();
            let identifier = base_id
                .split_once('/')
                .map(|split| split.0)
                .unwrap_or(&base_id);
            // NOTE: The `!` preceding the `*is_enabled` is important for inverting the logic since the
            // underlying property in the metrics.yaml is `disabled` and the outward API is treating it as
            // if it were `enabled` to make it easier to understand.

            if !remote_settings_config.metrics_enabled.is_empty() {
                if let Some(is_enabled) = remote_settings_config.metrics_enabled.get(identifier) {
                    u8::from(!*is_enabled)
                } else {
                    u8::from(self.meta().inner.disabled)
                }
            } else {
                u8::from(self.meta().inner.disabled)
            }
        };

        // Re-encode the epoch and enabled status and update the metadata
        let new_disabled = (remote_settings_epoch << 4) | (current_disabled & 0xF);
        self.meta().disabled.store(new_disabled, Ordering::Relaxed);

        // Return a boolean indicating whether or not the metric should be recorded
        current_disabled == 0
    }
}

impl Metric {
    /// Gets the ping section the metric fits into.
    ///
    /// This determines the section of the ping to place the metric data in when
    /// assembling the ping payload.
    pub fn ping_section(&self) -> &'static str {
        match self {
            Metric::Boolean(_) => "boolean",
            Metric::Counter(_) => "counter",
            // Custom distributions are in the same section, no matter what bucketing.
            Metric::CustomDistributionExponential(_) => "custom_distribution",
            Metric::CustomDistributionLinear(_) => "custom_distribution",
            Metric::Datetime(_, _) => "datetime",
            Metric::Experiment(_) => panic!("Experiments should not be serialized through this"),
            Metric::Quantity(_) => "quantity",
            Metric::Rate(..) => "rate",
            Metric::String(_) => "string",
            Metric::StringList(_) => "string_list",
            Metric::Timespan(..) => "timespan",
            Metric::TimingDistribution(_) => "timing_distribution",
            Metric::Url(_) => "url",
            Metric::Uuid(_) => "uuid",
            Metric::MemoryDistribution(_) => "memory_distribution",
            Metric::Jwe(_) => "jwe",
            Metric::Text(_) => "text",
            Metric::Object(_) => "object",
        }
    }

    /// The JSON representation of the metric's data
    pub fn as_json(&self) -> JsonValue {
        match self {
            Metric::Boolean(b) => json!(b),
            Metric::Counter(c) => json!(c),
            Metric::CustomDistributionExponential(hist) => {
                json!(custom_distribution::snapshot(hist))
            }
            Metric::CustomDistributionLinear(hist) => json!(custom_distribution::snapshot(hist)),
            Metric::Datetime(d, time_unit) => json!(get_iso_time_string(*d, *time_unit)),
            Metric::Experiment(e) => e.as_json(),
            Metric::Quantity(q) => json!(q),
            Metric::Rate(num, den) => {
                json!({"numerator": num, "denominator": den})
            }
            Metric::String(s) => json!(s),
            Metric::StringList(v) => json!(v),
            Metric::Timespan(time, time_unit) => {
                json!({"value": time_unit.duration_convert(*time), "time_unit": time_unit})
            }
            Metric::TimingDistribution(hist) => json!(timing_distribution::snapshot(hist)),
            Metric::Url(s) => json!(s),
            Metric::Uuid(s) => json!(s),
            Metric::MemoryDistribution(hist) => json!(memory_distribution::snapshot(hist)),
            Metric::Jwe(s) => json!(s),
            Metric::Text(s) => json!(s),
            Metric::Object(s) => {
                serde_json::from_str(s).expect("object storage should have been json")
            }
        }
    }
}