Repository: prometheus/client_python
Pull request #26 (2015-04-13 04:35:22)

Add the ability to specify labels as a dict of labelname->labelvalue.
This is useful for code that knows some of the labels in different places.

An example use case is a helper I'm trying to write that would work as such:

c = Counter('http_exceptions_total', 'help', ['method', 'exception_type'])
def handle_request(request):
    with count_exceptions_by_type(c, {'method': request.method}):
        do_something(request)

This helper only knows about the exception_type label and only passes
the other labels through.


Patch:
diff --git a/README.md b/README.md
index 35e36a6..1721208 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,15 @@ c.labels('get', '/').inc()
 c.labels('post', '/submit').inc()
 ```
 
+Labels can also be provided as a dict:
+
+```python
+from prometheus_client import Counter
+c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+c.labels({'method': 'get', 'endpoint': '/'}).inc()
+c.labels({'method': 'post', 'endpoint': '/submit'}).inc()
+```
+
 ### Process Collector
 
 The Python Client automatically exports metrics about process CPU usage, RAM,
diff --git a/prometheus_client/__init__.py b/prometheus_client/__init__.py
index a0fd65c..59af12d 100644
--- a/prometheus_client/__init__.py
+++ b/prometheus_client/__init__.py
@@ -111,10 +111,23 @@ class _LabelWrapper(object):
                 raise ValueError('Invalid label metric name: ' + l)
 
     def labels(self, *labelvalues):
-        '''Return the child for the given labelset.'''
-        if len(labelvalues) != len(self._labelnames):
-            raise ValueError('Incorrect label count')
-        labelvalues = tuple([unicode(l) for l in labelvalues])
+        '''Return the child for the given labelset.
+
+        Labels can be provided as a tuple or as a dict:
+            c = Counter('c', 'counter', ['l', 'm'])
+            # Set labels by position
+            c.labels('0', '1').inc()
+            # Set labels by name
+            c.labels({'l': '0', 'm': '1'}).inc()
+        '''
+        if len(labelvalues) == 1 and type(labelvalues[0]) == dict:
+            if sorted(labelvalues[0].keys()) != sorted(self._labelnames):
+                raise ValueError('Incorrect label names')
+            labelvalues = tuple([unicode(labelvalues[0][l]) for l in self._labelnames])
+        else:
+            if len(labelvalues) != len(self._labelnames):
+                raise ValueError('Incorrect label count')
+            labelvalues = tuple([unicode(l) for l in labelvalues])
         with self._lock:
             if labelvalues not in self._metrics:
                 self._metrics[labelvalues] = self._wrappedClass(**self._kwargs)


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 4945131..d28ed7a 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -234,7 +234,8 @@ class TestMetricWrapper(unittest.TestCase):
 
     def test_labels_coerced_to_string(self):
         self.counter.labels(None).inc()
-        self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'None'}))
+        self.counter.labels({'l': None}).inc()
+        self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'None'}))
 
         self.counter.remove(None)
         self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'None'}))
@@ -243,12 +244,27 @@ class TestMetricWrapper(unittest.TestCase):
         class Test(object):
             __str__ = None
         self.assertRaises(TypeError, self.counter.labels, Test())
+        self.assertRaises(TypeError, self.counter.labels, {'l': Test()})
 
     def test_namespace_subsystem_concatenated(self):
         c = Counter('c', 'help', namespace='a', subsystem='b', registry=self.registry)
         c.inc()
         self.assertEqual(1, self.registry.get_sample_value('a_b_c'))
 
+    def test_labels_by_dict(self):
+        self.counter.labels({'l': 'x'}).inc()
+        self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
+        self.assertRaises(ValueError, self.counter.labels, {'l': 'x', 'm': 'y'})
+        self.assertRaises(ValueError, self.counter.labels, {'m': 'y'})
+        self.assertRaises(ValueError, self.counter.labels, {})
+        self.two_labels.labels({'a': 'x', 'b': 'y'}).inc()
+        self.assertEqual(1, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
+        self.assertRaises(ValueError, self.two_labels.labels, {'a': 'x', 'b': 'y', 'c': 'z'})
+        self.assertRaises(ValueError, self.two_labels.labels, {'a': 'x', 'c': 'z'})
+        self.assertRaises(ValueError, self.two_labels.labels, {'b': 'y', 'c': 'z'})
+        self.assertRaises(ValueError, self.two_labels.labels, {'c': 'z'})
+        self.assertRaises(ValueError, self.two_labels.labels, {})
+
     def test_invalid_names_raise(self):
         self.assertRaises(ValueError, Counter, '', 'help')
         self.assertRaises(ValueError, Counter, '^', 'help')