Repository: prometheus/client_python (python)

Issue:
No error on missing labels for Gauge.set_function()
If I try to assign a function to a Gauge using `set_function()`, the Prometheus library does not error even if the labels are missing.

This code should error, but it does not:

```python
import prometheus_client

processed_input_messages = prometheus_client.Gauge(
    name="my_gauge",
    documentation="A generic Gauge.",
    labelnames=["a", "b"],
)

if __name__ == '__main__':
    # This should fail, because the "a" and "b" labels are not provided!
    processed_input_messages.set_function(lambda: 1)
```

In general, it would be important to check, for every metric type, if all the labels are set when calling `.set()`, `.inc()`, `.set_function()`, and the other similar metrics.

Pull request:
Raise the correct exception if the metrics are not observable
Always check that a metric is observable before changing its value. Fixes #665.

Patch:
diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py
index 2835993..062fecf 100644
--- a/prometheus_client/metrics.py
+++ b/prometheus_client/metrics.py
@@ -258,6 +258,7 @@ class Counter(MetricWrapperBase):
 
     def inc(self, amount=1):
         """Increment counter by the given amount."""
+        self._raise_if_not_observable()
         if amount < 0:
             raise ValueError('Counters can only be incremented by non-negative amounts.')
         self._value.inc(amount)
@@ -353,14 +354,17 @@ class Gauge(MetricWrapperBase):
 
     def inc(self, amount=1):
         """Increment gauge by the given amount."""
+        self._raise_if_not_observable()
         self._value.inc(amount)
 
     def dec(self, amount=1):
         """Decrement gauge by the given amount."""
+        self._raise_if_not_observable()
         self._value.inc(-amount)
 
     def set(self, value):
         """Set gauge to the given value."""
+        self._raise_if_not_observable()
         self._value.set(float(value))
 
     def set_to_current_time(self):
@@ -392,6 +396,8 @@ class Gauge(MetricWrapperBase):
         multiple threads. All other methods of the Gauge become NOOPs.
         """
 
+        self._raise_if_not_observable()
+
         def samples(self):
             return (('', {}, float(f())),)
 
@@ -450,6 +456,7 @@ class Summary(MetricWrapperBase):
         https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
         for details.
         """
+        self._raise_if_not_observable()
         self._count.inc(1)
         self._sum.inc(amount)
 
@@ -567,6 +574,7 @@ class Histogram(MetricWrapperBase):
         https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
         for details.
         """
+        self._raise_if_not_observable()
         self._sum.inc(amount)
         for i, bound in enumerate(self._upper_bounds):
             if amount <= bound:


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 55c88b7..dbb2703 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -19,6 +19,21 @@ except ImportError:
     import unittest
 
 
+def assert_not_observable(fn, *args, **kwargs):
+    """
+    Assert that a function call falls with a ValueError exception containing
+    'missing label values'
+    """
+
+    try:
+        fn(*args, **kwargs)
+    except ValueError as e:
+        assert 'missing label values' in str(e)
+        return
+
+    assert False, "Did not raise a 'missing label values' exception"
+
+
 class TestCounter(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
@@ -36,7 +51,6 @@ class TestCounter(unittest.TestCase):
 
     def test_negative_increment_raises(self):
         self.assertRaises(ValueError, self.counter.inc, -1)
-    
 
     def test_function_decorator(self):
         @self.counter.count_exceptions(ValueError)
@@ -76,18 +90,21 @@ class TestCounter(unittest.TestCase):
 
     def test_count_exceptions_not_observable(self):
         counter = Counter('counter', 'help', labelnames=('label',), registry=self.registry)
+        assert_not_observable(counter.count_exceptions)
 
-        try:
-            counter.count_exceptions()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+    def test_inc_not_observable(self):
+        """.inc() must fail if the counter is not observable."""
+
+        counter = Counter('counter', 'help', labelnames=('label',), registry=self.registry)
+        assert_not_observable(counter.inc)
 
 
 class TestGauge(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
         self.gauge = Gauge('g', 'help', registry=self.registry)
-    
+        self.gauge_with_label = Gauge('g2', 'help', labelnames=("label1",), registry=self.registry)
+
     def test_repr(self):
         self.assertEqual(repr(self.gauge), "prometheus_client.metrics.Gauge(g)")
 
@@ -100,6 +117,21 @@ class TestGauge(unittest.TestCase):
         self.gauge.set(9)
         self.assertEqual(9, self.registry.get_sample_value('g'))
 
+    def test_inc_not_observable(self):
+        """.inc() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.inc)
+
+    def test_dec_not_observable(self):
+        """.dec() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.dec)
+
+    def test_set_not_observable(self):
+        """.set() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.set, 1)
+
     def test_inprogress_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('g'))
 
@@ -127,6 +159,11 @@ class TestGauge(unittest.TestCase):
         x['a'] = None
         self.assertEqual(1, self.registry.get_sample_value('g'))
 
+    def test_set_function_not_observable(self):
+        """.set_function() must fail if the gauge is not observable."""
+
+        assert_not_observable(self.gauge_with_label.set_function, lambda: 1)
+
     def test_time_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('g'))
 
@@ -167,25 +204,18 @@ class TestGauge(unittest.TestCase):
 
     def test_track_in_progress_not_observable(self):
         g = Gauge('test', 'help', labelnames=('label',), registry=self.registry)
-
-        try:
-            g.track_inprogress()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+        assert_not_observable(g.track_inprogress)
 
     def test_timer_not_observable(self):
         g = Gauge('test', 'help', labelnames=('label',), registry=self.registry)
-
-        try:
-            g.time()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+        assert_not_observable(g.time)
 
 
 class TestSummary(unittest.TestCase):
     def setUp(self):
         self.registry = CollectorRegistry()
         self.summary = Summary('s', 'help', registry=self.registry)
+        self.summary_with_labels = Summary('s_with_labels', 'help', labelnames=("label1",), registry=self.registry)
 
     def test_repr(self):
         self.assertEqual(repr(self.summary), "prometheus_client.metrics.Summary(s)")
@@ -197,6 +227,10 @@ class TestSummary(unittest.TestCase):
         self.assertEqual(1, self.registry.get_sample_value('s_count'))
         self.assertEqual(10, self.registry.get_sample_value('s_sum'))
 
+    def test_summary_not_observable(self):
+        """.observe() must fail if the Summary is not observable."""
+        assert_not_observable(self.summary_with_labels.observe, 1)
+
     def test_function_decorator(self):
         self.assertEqual(0, self.registry.get_sample_value('s_count'))
 
@@ -267,10 +301,7 @@ class TestSummary(unittest.TestCase):
     def test_timer_not_observable(self):
         s = Summary('test', 'help', labelnames=('label',), registry=self.registry)
 
-        try:
-            s.time()
-        except ValueError as e:
-            self.assertIn('missing label values', str(e))
+        assert_not_observable(s.time)
 
 
 class TestHistogram(unittest.TestCase):
@@ -315,6 +346,10 @@ class TestHistogram(unittest.TestCase):
         self.assertEqual(3, self.registry.get_sample_value('h_count'))
         self.assertEqual(float("inf"), self.registry.get_sample_value('h_sum'))
 
+    def test_histogram_not_observable(self):
+        """.observe() must fail if the Summary is not observable."""
+        assert_not_observable(self.labels.observe, 1)
+
     def test_setting_buckets(self):
         h = Histogram('h', 'help', registry=None, buckets=[0, 1, 2])
         self.assertEqual([0.0, 1.0, 2.0, float("inf")], h._upper_bounds)