Repository: prometheus/client_python
Pull request #453 (2019-08-13 13:02:19)

Add target_info to registries
This allows target labels as needed by push-based systems to be
provided, in a way that doesn't mess up Prometheus's own
top-down pull based approach to SD.

@SuperQ @RichiH @robskillington @sumeer Here's roughly what I'm thinking. Needs unittests etc.

@beorn7 Your input would be appreciated on the pgw front. https://github.com/OpenObservability/OpenMetrics/issues/63 is the general background. Pgw could continue to do this out of band for grouping labels (potentially with the client library using this as a default), or have this as an option.

beorn7: I don't see a blocker from the Pushgateway side. There are many options how the PGW could incorporate this. If it's not the client library taking the target_info as a default for the grouping key, it could even be done by the PGW itself.
brian-brazil: I've added unittests, and a basic check that we're not exporting two target_infos.
robskillington: Hm, I was able to reproduce the issue I described in the comment I left:
```
>>> import prometheus_client
>>> registry = prometheus_client.CollectorRegistry()
>>> registry.set_target_info({'foo':'bar'})
>>> results = registry.restricted_registry(['target_info'])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "prometheus_client/registry.py", line 105, in restricted_registry
    for metric in collector.collect():
AttributeError: 'NoneType' object has no attribute 'collect'
```
brian-brazil: Right you are, fixed and unittest added.

Patch:
diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py
index fa2717f..7f86a00 100644
--- a/prometheus_client/registry.py
+++ b/prometheus_client/registry.py
@@ -12,11 +12,13 @@ class CollectorRegistry(object):
     exposition formats.
     """
 
-    def __init__(self, auto_describe=False):
+    def __init__(self, auto_describe=False, target_info=None):
         self._collector_to_names = {}
         self._names_to_collectors = {}
         self._auto_describe = auto_describe
         self._lock = Lock()
+        self._target_info = {}
+        self.set_target_info(target_info)
 
     def register(self, collector):
         """Add a collector to the registry."""
@@ -69,8 +71,13 @@ class CollectorRegistry(object):
     def collect(self):
         """Yields metrics from the collectors in the registry."""
         collectors = None
+        ti = None
         with self._lock:
             collectors = copy.copy(self._collector_to_names)
+            if self._target_info:
+                ti = self._target_info_metric()
+        if ti:
+            yield ti
         for collector in collectors:
             for metric in collector.collect():
                 yield metric
@@ -87,11 +94,14 @@ class CollectorRegistry(object):
         Experimental."""
         names = set(names)
         collectors = set()
+        metrics = []
         with self._lock:
+            if 'target_info' in names and self._target_info:
+                metrics.append(self._target_info_metric())
+                names.remove('target_info')
             for name in names:
                 if name in self._names_to_collectors:
                     collectors.add(self._names_to_collectors[name])
-        metrics = []
         for collector in collectors:
             for metric in collector.collect():
                 samples = [s for s in metric.samples if s[0] in names]
@@ -106,6 +116,25 @@ class CollectorRegistry(object):
 
         return RestrictedRegistry()
 
+    def set_target_info(self, labels):
+        with self._lock:
+            if labels:
+                if not self._target_info and 'target_info' in self._names_to_collectors:
+                    raise ValueError('CollectorRegistry already contains a target_info metric')
+                self._names_to_collectors['target_info'] = None
+            elif self._target_info:
+                self._names_to_collectors.pop('target_info', None)
+            self._target_info = labels
+
+    def get_target_info(self):
+        with self._lock:
+            return self._target_info
+
+    def _target_info_metric(self):
+        m = Metric('target', 'Target metadata', 'info')
+        m.add_sample('target_info', self._target_info, 1)
+        return m
+
     def get_sample_value(self, name, labels=None):
         """Returns the sample value, or None if not found.
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 83aabc0..1c0baed 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -696,6 +696,9 @@ class TestCollectorRegistry(unittest.TestCase):
         # The name of the histogram itself isn't taken.
         Gauge('h', 'help', registry=registry)
 
+        Info('i', 'help', registry=registry)
+        self.assertRaises(ValueError, Gauge, 'i_info', 'help', registry=registry)
+
     def test_unregister_works(self):
         registry = CollectorRegistry()
         s = Summary('s', 'help', registry=registry)
@@ -728,6 +731,34 @@ class TestCollectorRegistry(unittest.TestCase):
         m.samples = [Sample('s_sum', {}, 7)]
         self.assertEquals([m], registry.restricted_registry(['s_sum']).collect())
 
+    def test_target_info_injected(self):
+        registry = CollectorRegistry(target_info={'foo': 'bar'})
+        self.assertEqual(1, registry.get_sample_value('target_info', {'foo': 'bar'}))
+
+    def test_target_info_duplicate_detected(self):
+        registry = CollectorRegistry(target_info={'foo': 'bar'})
+        self.assertRaises(ValueError, Info, 'target', 'help', registry=registry)
+
+        registry.set_target_info({})
+        i = Info('target', 'help', registry=registry)
+        registry.set_target_info({})
+        self.assertRaises(ValueError, Info, 'target', 'help', registry=registry)
+        self.assertRaises(ValueError, registry.set_target_info, {'foo': 'bar'})
+        registry.unregister(i)
+        registry.set_target_info({'foo': 'bar'})
+
+    def test_target_info_restricted_registry(self):
+        registry = CollectorRegistry(target_info={'foo': 'bar'})
+        Summary('s', 'help', registry=registry).observe(7)
+
+        m = Metric('s', 'help', 'summary')
+        m.samples = [Sample('s_sum', {}, 7)]
+        self.assertEquals([m], registry.restricted_registry(['s_sum']).collect())
+
+        m = Metric('target', 'Target metadata', 'info')
+        m.samples = [Sample('target_info', {'foo': 'bar'}, 1)]
+        self.assertEquals([m], registry.restricted_registry(['target_info']).collect())
+
 
 if __name__ == '__main__':
     unittest.main()