# Adding a new metric type - Python

## FFI

Python can use the generated C header file directly, through [`cffi`](https://cffi.readthedocs.io/).
Re-generate it with

```sh
make cbindgen
```

## Python API

Finally, create a platform-specific metric type wrapper.
For Python this would be `glean-core/python/glean/metrics/counter.py`:

```python
class CounterMetricType:
    """
    This implements the developer facing API for recording counter metrics.

    Instances of this class type are automatically generated by
    `glean.load_metrics`, allowing developers to record values that were
    previously registered in the metrics.yaml file.

    The counter API only exposes the `CounterMetricType.add` method, which
    takes care of validating the input data and making sure that limits are
    enforced.
    """

    def __init__(
        self,
        disabled: bool,
        category: str,
        lifetime: Lifetime,
        name: str,
        send_in_pings: List[str],
    ):
        self._disabled = disabled
        self._send_in_pings = send_in_pings

        self._handle = _ffi.lib.glean_new_counter_metric(
            _ffi.ffi_encode_string(category),
            _ffi.ffi_encode_string(name),
            _ffi.ffi_encode_vec_string(send_in_pings),
            len(send_in_pings),
            lifetime.value,
            disabled,
        )

    def __del__(self):
        if getattr(self, "_handle", 0) != 0:
            _ffi.lib.glean_destroy_counter_metric(self._handle)

    def add(self, amount: int = 1):
        """
        Add to counter value.

        Args:
            amount (int): (default: 1) This is the amount to increment the
                counter by.
        """
        if self._disabled:
            return

        @Dispatcher.launch
        def add():
            _ffi.lib.glean_counter_add(self._handle, amount)

    def test_has_value(self, ping_name: Optional[str] = None) -> bool:
        """
        Tests whether a value is stored for the metric for testing purposes
        only.

        Args:
            ping_name (str): (default: first value in send_in_pings) The name
                of the ping to retrieve the metric for.

        Returns:
            has_value (bool): True if the metric value exists.
        """
        if ping_name is None:
            ping_name = self._send_in_pings[0]

        return bool(
            _ffi.lib.glean_counter_test_has_value(
                self._handle, _ffi.ffi_encode_string(ping_name)
            )
        )

    def test_get_value(self, ping_name: Optional[str] = None) -> int:
        """
        Returns the stored value for testing purposes only.

        Args:
            ping_name (str): (default: first value in send_in_pings) The name
                of the ping to retrieve the metric for.

        Returns:
            value (int): value of the stored metric.
        """
        if ping_name is None:
            ping_name = self._send_in_pings[0]

        if not self.test_has_value(ping_name):
            raise ValueError("metric has no value")

        return _ffi.lib.glean_counter_test_get_value(
            self._handle, _ffi.ffi_encode_string(ping_name)
        )

    def test_get_num_recorded_errors(
        self, error_type: ErrorType, ping_name: Optional[str] = None
    ) -> int:
        """
        Returns the number of errors recorded for the given metric.

        Args:
            error_type (ErrorType): The type of error recorded.
            ping_name (str): (default: first value in send_in_pings) The name
                of the ping to retrieve the metric for.

        Returns:
            num_errors (int): The number of errors recorded for the metric for
                the given error type.
        """
        if ping_name is None:
            ping_name = self._send_in_pings[0]

        return _ffi.lib.glean_counter_test_get_num_recorded_errors(
            self._handle, error_type.value, _ffi.ffi_encode_string(ping_name),
        )
```

The new metric type also needs to be imported from `glean-core/python/glean/metrics/__init__.py`:

```python
from .counter import CounterMetricType


__all__ = [
    "CounterMetricType",
    # ...
]
```

It also must be added to the `_TYPE_MAPPING` in `glean-core/python/glean/_loader.py`:

```python
_TYPE_MAPPING = {
    "counter": metrics.CounterMetricType,
    # ...
}
```
