# Adding a new metric type

Data in the Glean SDK is stored in so-called metrics. You can find the full list of implemented metric types in the user overview.

Adding a new metric type involves defining the metric type's API, its persisted and in-memory storage as well as its serialization into the ping payload.

## glean_parser

In order for your metric to be usable, you must add it to glean_parser so that instances of your new metric can be instantiated and available to our users.

The documentation for how to do this should live in the glean_parser repository, but in short:

• Your metric type must be added as a type in the object model
• Any new parameters outside of the common metric data must also be added to the schema, and be stored in the object model.

## The metric type's API

A new metric type is defined in glean-core/src/glean.udl. Each metric type is its own interface with a constructor and all recording and testing functions defined. It supports built-in types as well as new custom types. See the UniFFI documentation for more.

interface CounterMetric {
constructor(CommonMetricData meta);

void add(optional i32 amount = 1);
};


The implementation of this metric type is defined in its own file under glean-core/src/metrics/, e.g. glean-core/src/metrics/counter.rs for a Counter.

Start by defining a structure to hold the metric's metadata:

#[derive(Clone, Debug)]
pub struct CounterMetric {
meta: Arc<CommonMetricData>,
}


Implement the MetricType trait to create a metric from the meta data as well as expose the meta data. This also gives you a should_record method on the metric type.

impl MetricType for CounterMetric {
fn meta(&self) -> &CommonMetricData {
&self.meta
}
}


Its implementation should have a way to create a new metric from the common metric data. It should be the similar for all metric types. Additional metric type parameters are passed as additional arguments.

impl CounterMetric {
pub fn new(meta: CommonMetricData) -> Self {
Self {
meta: Arc::new(meta),
}
}
}


Implement each method for the type. The public method should do minimal work synchronously and defer logic & storage functionality to run on the dispatcher. The synchronous implementation should then take glean: &Glean to be able to access the storage.

impl CounterMetric { // same block as above
pub fn add(&self, amount: i32) {
let metric = self.clone();
}

fn add_sync(&self, glean: &Glean, amount: i32) {
// Always include this check!
if !self.should_record() {
return;
}

// Do error handling here

glean
.storage()
.record_with(&self.meta, |old_value| match old_value {
Some(Metric::Counter(old_value)) => Metric::Counter(old_value + amount),
_ => Metric::Counter(amount),
})
}
}


Use glean.storage().record() to record a fixed value or glean.storage.record_with() to construct a new value from the currently stored one.

The storage operation makes use of the metric's variant of the Metric enumeration.

## The Metric enumeration

Persistence and in-memory serialization as well as ping payload serialization are handled through the Metric enumeration. This is defined in glean-core/src/metrics/mod.rs. Variants of this enumeration are used in the storage implementation of the metric type.

To add a new metric type, include the metric module and declare its use, then add a new variant to the Metric enum:


mod counter;

// ...

pub use self::counter::CounterMetric;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Metric {
// ...
Counter(i32),
}


Then modify the below implementation and define the right ping section name for the new type. This will be used in the ping payload:

impl Metric {
pub fn ping_section(&self) -> &'static str {
match self {
// ...
Metric::Counter(_) => "counter",
}
}
}


Finally, define the ping payload serialization (as JSON). In the simple cases where the in-memory representation maps to its JSON representation it is enough to call the json! macro.

impl Metric { // same block as above
pub fn as_json(&self) -> JsonValue {
match self {
// ...
Metric::Counter(c) => json!(c),
}
}
}


For more complex serialization consider implementing serialization logic as a function returning a serde_json::Value or another object that can be serialized.

For example, the DateTime serializer has the following entry, where get_iso_time_string is a function to convert from the DateTime metric representation to a string:

Metric::Datetime(d, time_unit) => json!(get_iso_time_string(*d, *time_unit)),


## Documentation

Documentation for the new metric type must be added to the user book.

• Add a new file for your new metric in docs/user/reference/metrics/. Its contents should follow the form and content of the other examples in that folder.
• Reference that file in docs/user/SUMMARY.md so it will be included in the build.
• Follow the Documentation Contribution Guide.

You must also update the payload documentation with how the metric looks in the payload.

## Tests

Tests are written in the Language Bindings and tend to just cover basic functionality:

• The metric returns the correct value when it has no value
• The metric correctly reports errors
• The metric returns the correct value when it has value

At this point the metric type will have an auto-generated API in all target languages. This needs to be re-exported in the target language. The following chapters have details on how to do that for the different languages.

Sometimes a metric type needs some additional modifications to expose a language-specific type, apply additional type conversions or add additional functionality. For how to implement additional modifications to the API see the following chapters.