Repository: prometheus/client_python

Issue:
CounterMetricFamily should support exemplars
Exemplars are specified as being for Histogram and Counter types, and supported by this client for Histogram and Counter types.

However for custom collectors HistogramMetricFamily supports exemplars, but CounterMetricFamily does not.

Support should be added to CounterMetricFamily.

This shouldn't be too hard, it was added to the underlying layers for HistogramMetricFamily support.

Discussion:
@lod: Workaround until the MR is released.

```python
from typing import Optional, Union, Sequence, Timestamp
from prometheus_client.core import CounterMetricFamily, Exemplar, Sample


class MyCounterMetricFamily(CounterMetricFamily):
    """
    Standard class doesn't support exemplars, so we hack it in.

    See https://github.com/prometheus/client_python/issues/1062
    """

    def __init__(
        self,
        name: str,
        documentation: str,
        value: Optional[float] = None,
        labels: Optional[Sequence[str]] = None,
        created: Optional[float] = None,
        unit: str = "",
        exemplar: Optional[Exemplar] = None,
    ):
        # Run normal, but don't pass in value
        CounterMetricFamily.__init__(self, name, documentation, None, labels, created, unit)

        if value is not None:
            self.add_metric([], value, created, exemplar=exemplar)

    def add_metric(
        self,
        labels: Sequence[str],
        value: float,
        created: Optional[float] = None,
        timestamp: Optional[Union[Timestamp, float]] = None,
        exemplar: Optional[Exemplar] = None,
    ) -> None:
        self.samples.append(
            Sample(self.name + "_total", dict(zip(self._labelnames, labels)), value, timestamp, exemplar)
        )
        if created is not None:
            self.samples.append(Sample(self.name + "_created", dict(zip(self._labelnames, labels)), created, timestamp))
```

Solution patch:
diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py
index 19166e1..b09cea0 100644
--- a/prometheus_client/metrics_core.py
+++ b/prometheus_client/metrics_core.py
@@ -116,6 +116,7 @@ class CounterMetricFamily(Metric):
                  labels: Optional[Sequence[str]] = None,
                  created: Optional[float] = None,
                  unit: str = '',
+                 exemplar: Optional[Exemplar] = None,
                  ):
         # Glue code for pre-OpenMetrics metrics.
         if name.endswith('_total'):
@@ -127,13 +128,14 @@ class CounterMetricFamily(Metric):
             labels = []
         self._labelnames = tuple(labels)
         if value is not None:
-            self.add_metric([], value, created)
+            self.add_metric([], value, created, exemplar=exemplar)
 
     def add_metric(self,
                    labels: Sequence[str],
                    value: float,
                    created: Optional[float] = None,
                    timestamp: Optional[Union[Timestamp, float]] = None,
+                   exemplar: Optional[Exemplar] = None,
                    ) -> None:
         """Add a metric to the metric family.
 
@@ -142,7 +144,7 @@ class CounterMetricFamily(Metric):
           value: The value of the metric
           created: Optional unix timestamp the child was created at.
         """
-        self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp))
+        self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp, exemplar))
         if created is not None:
             self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp))
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index f80fb88..056d8e5 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -724,6 +724,21 @@ class TestMetricFamilies(unittest.TestCase):
         self.custom_collector(cmf)
         self.assertEqual(2, self.registry.get_sample_value('c_total', {'a': 'b', 'c_total': 'd'}))
 
+    def test_counter_exemplars_oneline(self):
+        cmf = CounterMetricFamily('c_total', 'help', value=23, exemplar={"bob": "osbourne"})
+        self.custom_collector(cmf)
+        sample = [c.samples for c in self.registry.collect()][0][0]
+        self.assertDictEqual({"bob": "osbourne"}, sample.exemplar)
+
+    def test_counter_exemplars_add(self):
+        cmf = CounterMetricFamily('c_total', 'help')
+        cmf.add_metric([], 12, exemplar={"bob": "osbourne"}, created=23)
+        self.custom_collector(cmf)
+        total_sample, created_sample = [c.samples for c in self.registry.collect()][0]
+        self.assertEqual("c_created", created_sample.name)
+        self.assertDictEqual({"bob": "osbourne"}, total_sample.exemplar)
+        self.assertIsNone(created_sample.exemplar)
+
     def test_gauge(self):
         self.custom_collector(GaugeMetricFamily('g', 'help', value=1))
         self.assertEqual(1, self.registry.get_sample_value('g', {}))