Repository: prometheus/client_python
Pull request #804 (2022-05-03 15:17:55)

Support metric help text in multiprocess mode
Issue #211
Add support for storing the metrics help text in the multiprocess map.
The help will come from the first process read, but it should be the
same for all metrics.

@csmarchbanks not sure if i missed anything but let me know if you need anything changed



Patch:
diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py
index 8878fb8..45125b4 100644
--- a/prometheus_client/metrics.py
+++ b/prometheus_client/metrics.py
@@ -275,7 +275,7 @@ class Counter(MetricWrapperBase):
 
     def _metric_init(self) -> None:
         self._value = values.ValueClass(self._type, self._name, self._name + '_total', self._labelnames,
-                                        self._labelvalues)
+                                        self._labelvalues, self._documentation)
         self._created = time.time()
 
     def inc(self, amount: float = 1, exemplar: Optional[Dict[str, str]] = None) -> None:
@@ -377,7 +377,7 @@ class Gauge(MetricWrapperBase):
     def _metric_init(self) -> None:
         self._value = values.ValueClass(
             self._type, self._name, self._name, self._labelnames, self._labelvalues,
-            multiprocess_mode=self._multiprocess_mode
+            self._documentation, multiprocess_mode=self._multiprocess_mode
         )
 
     def inc(self, amount: float = 1) -> None:
@@ -469,8 +469,8 @@ class Summary(MetricWrapperBase):
 
     def _metric_init(self) -> None:
         self._count = values.ValueClass(self._type, self._name, self._name + '_count', self._labelnames,
-                                        self._labelvalues)
-        self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues)
+                                        self._labelvalues, self._documentation)
+        self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues, self._documentation)
         self._created = time.time()
 
     def observe(self, amount: float) -> None:
@@ -583,14 +583,15 @@ class Histogram(MetricWrapperBase):
         self._buckets: List[values.ValueClass] = []
         self._created = time.time()
         bucket_labelnames = self._labelnames + ('le',)
-        self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues)
+        self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues, self._documentation)
         for b in self._upper_bounds:
             self._buckets.append(values.ValueClass(
                 self._type,
                 self._name,
                 self._name + '_bucket',
                 bucket_labelnames,
-                self._labelvalues + (floatToGoString(b),))
+                self._labelvalues + (floatToGoString(b),),
+                self._documentation)
             )
 
     def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> None:
diff --git a/prometheus_client/mmap_dict.py b/prometheus_client/mmap_dict.py
index e8b2df9..2bdd6ef 100644
--- a/prometheus_client/mmap_dict.py
+++ b/prometheus_client/mmap_dict.py
@@ -2,6 +2,7 @@ import json
 import mmap
 import os
 import struct
+from typing import List
 
 _INITIAL_MMAP_SIZE = 1 << 20
 _pack_integer_func = struct.Struct(b'i').pack
@@ -137,8 +138,8 @@ class MmapedDict:
             self._f = None
 
 
-def mmap_key(metric_name, name, labelnames, labelvalues):
+def mmap_key(metric_name: str, name: str, labelnames: List[str], labelvalues: List[str], help_text: str) -> str:
     """Format a key for use in the mmap file."""
     # ensure labels are in consistent order for identity
     labels = dict(zip(labelnames, labelvalues))
-    return json.dumps([metric_name, name, labels], sort_keys=True)
+    return json.dumps([metric_name, name, labels, help_text], sort_keys=True)
diff --git a/prometheus_client/multiprocess.py b/prometheus_client/multiprocess.py
index 5a23c48..dd34391 100644
--- a/prometheus_client/multiprocess.py
+++ b/prometheus_client/multiprocess.py
@@ -15,8 +15,6 @@ try:  # Python3
 except NameError:  # Python >= 2.5
     FileNotFoundError = IOError
 
-MP_METRIC_HELP = 'Multiprocess metric'
-
 
 class MultiProcessCollector:
     """Collector for files for multi-process mode."""
@@ -53,9 +51,9 @@ class MultiProcessCollector:
         def _parse_key(key):
             val = key_cache.get(key)
             if not val:
-                metric_name, name, labels = json.loads(key)
+                metric_name, name, labels, help_text = json.loads(key)
                 labels_key = tuple(sorted(labels.items()))
-                val = key_cache[key] = (metric_name, name, labels, labels_key)
+                val = key_cache[key] = (metric_name, name, labels, labels_key, help_text)
             return val
 
         for f in files:
@@ -71,11 +69,11 @@ class MultiProcessCollector:
                     continue
                 raise
             for key, value, _ in file_values:
-                metric_name, name, labels, labels_key = _parse_key(key)
+                metric_name, name, labels, labels_key, help_text = _parse_key(key)
 
                 metric = metrics.get(metric_name)
                 if metric is None:
-                    metric = Metric(metric_name, MP_METRIC_HELP, typ)
+                    metric = Metric(metric_name, help_text, typ)
                     metrics[metric_name] = metric
 
                 if typ == 'gauge':
diff --git a/prometheus_client/values.py b/prometheus_client/values.py
index 03b203b..3373379 100644
--- a/prometheus_client/values.py
+++ b/prometheus_client/values.py
@@ -10,7 +10,7 @@ class MutexValue:
 
     _multiprocess = False
 
-    def __init__(self, typ, metric_name, name, labelnames, labelvalues, **kwargs):
+    def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, **kwargs):
         self._value = 0.0
         self._exemplar = None
         self._lock = Lock()
@@ -57,8 +57,8 @@ def MultiProcessValue(process_identifier=os.getpid):
 
         _multiprocess = True
 
-        def __init__(self, typ, metric_name, name, labelnames, labelvalues, multiprocess_mode='', **kwargs):
-            self._params = typ, metric_name, name, labelnames, labelvalues, multiprocess_mode
+        def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, multiprocess_mode='', **kwargs):
+            self._params = typ, metric_name, name, labelnames, labelvalues, help_text, multiprocess_mode
             # This deprecation warning can go away in a few releases when removing the compatibility
             if 'prometheus_multiproc_dir' in os.environ and 'PROMETHEUS_MULTIPROC_DIR' not in os.environ:
                 os.environ['PROMETHEUS_MULTIPROC_DIR'] = os.environ['prometheus_multiproc_dir']
@@ -69,7 +69,7 @@ def MultiProcessValue(process_identifier=os.getpid):
                 values.append(self)
 
         def __reset(self):
-            typ, metric_name, name, labelnames, labelvalues, multiprocess_mode = self._params
+            typ, metric_name, name, labelnames, labelvalues, help_text, multiprocess_mode = self._params
             if typ == 'gauge':
                 file_prefix = typ + '_' + multiprocess_mode
             else:
@@ -81,7 +81,7 @@ def MultiProcessValue(process_identifier=os.getpid):
 
                 files[file_prefix] = MmapedDict(filename)
             self._file = files[file_prefix]
-            self._key = mmap_key(metric_name, name, labelnames, labelvalues)
+            self._key = mmap_key(metric_name, name, labelnames, labelvalues, help_text)
             self._value = self._file.read_value(self._key)
 
         def __check_for_pid_change(self):


Test patch:
diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py
index a41903a..10990ad 100644
--- a/tests/test_multiprocess.py
+++ b/tests/test_multiprocess.py
@@ -281,6 +281,31 @@ class TestMultiProcess(unittest.TestCase):
 
         self.assertEqual(metrics['h'].samples, expected_histogram)
 
+    def test_collect_preserves_help(self):
+        pid = 0
+        values.ValueClass = MultiProcessValue(lambda: pid)
+        labels = {i: i for i in 'abcd'}
+
+        c = Counter('c', 'c help', labelnames=labels.keys(), registry=None)
+        g = Gauge('g', 'g help', labelnames=labels.keys(), registry=None)
+        h = Histogram('h', 'h help', labelnames=labels.keys(), registry=None)
+
+        c.labels(**labels).inc(1)
+        g.labels(**labels).set(1)
+        h.labels(**labels).observe(1)
+
+        pid = 1
+
+        c.labels(**labels).inc(1)
+        g.labels(**labels).set(1)
+        h.labels(**labels).observe(5)
+
+        metrics = {m.name: m for m in self.collector.collect()}
+
+        self.assertEqual(metrics['c'].documentation, 'c help')
+        self.assertEqual(metrics['g'].documentation, 'g help')
+        self.assertEqual(metrics['h'].documentation, 'h help')
+
     def test_merge_no_accumulate(self):
         pid = 0
         values.ValueClass = MultiProcessValue(lambda: pid)