Repository: prometheus/client_python
Pull request #59 (2015-10-14 12:13:16)

Add a twisted resource for exposing metrics via twisted.web.
I'm not sure if this belongs as part of the client, or should be an external package that depends on this.

tomprince: Done.
brian-brazil: That looks good, can you squash your commits please?

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 09f2510..daa8ceb 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -7,5 +7,3 @@ The following individuals have contributed code to this repository
 
 * Andrea Fagan <andreafagan28@gmail.com>
 * Brian Brazil <brian.brazil@gmail.com>
-* Paul Logston <code@logston.me>
-
diff --git a/README.md b/README.md
index 5601b12..3020b58 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ pip install prometheus_client
 
 **Two**: Paste the following into a Python interpreter:
 ```python
-from prometheus_client import start_http_server, Summary
+from prometheus_client import start_http_server,Summary
 import random
 import time
 
@@ -219,16 +219,33 @@ server in a daemon thread on the given port:
 
 ```python
 from prometheus_client import start_http_server
-
 start_http_server(8000)
 ```
 
 Visit [http://localhost:8000/](http://localhost:8000/) to view the metrics.
 
-To add Prometheus exposition to an existing HTTP server, see the `MetricsHandler` class
+To add Prometheus exposition to an existing HTTP server, see the `MetricsServlet` class
 which provides a `BaseHTTPRequestHandler`. It also serves as a simple example of how
 to write a custom endpoint.
 
+#### Twisted
+
+To use prometheus with [twisted](https://twistedmatrix.com/), there is `MetricsResource` which exposes metrics as a twisted resource.
+
+```python
+from prometheus_client.twisted import MetricsResource
+from twisted.web.server import Site
+from twisted.web.resource import Resource
+from twisted.internet import reactor
+
+root = Resource()
+root.putChild(b'metrics', MetricsResource())
+
+factory = Site(root)
+reactor.listenTCP(8000, factory)
+reactor.run()
+```
+
 ### Node exporter textfile collector
 
 The [textfile collector](https://github.com/prometheus/node_exporter#textfile-collector)
@@ -239,8 +256,7 @@ about a machine system that the Node exporter does not support or would not make
 to perform at every scrape (for example, anything involving subprocesses).
 
 ```python
-from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
-
+from prometheus_client import CollectorRegistry,Gauge,write_to_textfile
 registry = CollectorRegistry()
 g = Gauge('raid_status', '1 if raid array is okay', registry=registry)
 g.set(1)
@@ -256,8 +272,7 @@ The [Pushgateway](https://github.com/prometheus/pushgateway)
 allows ephemeral and batch jobs to expose their metrics to Prometheus.
 
 ```python
-from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
-
+from prometheus_client import CollectorRegistry,Gauge,push_to_gateway
 registry = CollectorRegistry()
 g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finished', registry=registry)
 g.set_to_current_time()
@@ -290,7 +305,6 @@ Metrics are pushed over TCP in the Graphite plaintext format.
 
 ```python
 from prometheus_client.bridge.graphite import GraphiteBridge
-
 gb = GraphiteBridge(('graphite.your.org', 2003))
 # Push once.
 gb.push()
@@ -329,10 +343,11 @@ This is intended for advanced use cases where you have servers
 exposing Prometheus metrics and need to get them into some other
 system.
 
-```python
+```
 from prometheus_client.parser import text_string_to_metric_families
 for family in text_string_to_metric_families("my_gauge 1.0\n"):
   for sample in family.samples:
     print("Name: {0} Labels: {1} Value: {2}".format(*sample))
 ```
 
+
diff --git a/prometheus_client/core.py b/prometheus_client/core.py
index 720efe6..14b5394 100644
--- a/prometheus_client/core.py
+++ b/prometheus_client/core.py
@@ -256,25 +256,12 @@ class _LabelWrapper(object):
     def labels(self, *labelvalues):
         '''Return the child for the given labelset.
 
-        All metrics can have labels, allowing grouping of related time series.
-        Taking a counter as an example:
-
-            from prometheus_client import Counter
-
-            c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
-            c.labels('get', '/').inc()
-            c.labels('post', '/submit').inc()
-
-        Labels can also be provided as a dict:
-
-            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()
-
-        See the best practices on [naming](http://prometheus.io/docs/practices/naming/)
-        and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels).
+        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):
@@ -360,24 +347,10 @@ class Counter(object):
 
     An example for a Counter:
 
-        from prometheus_client import Counter
-
-        c = Counter('my_failures_total', 'Description of counter')
-        c.inc()     # Increment by 1
-        c.inc(1.6)  # Increment by given value
-
-    There are utilities to count exceptions raised:
-
-        @c.count_exceptions()
-        def f():
-            pass
-
-        with c.count_exceptions():
-            pass
-
-        # Count only one type of exception
-        with c.count_exceptions(ValueError):
-            pass
+    from prometheus_client import Counter
+    c = Counter('my_failures_total', 'Description of counter')
+    c.inc()     # Increment by 1
+    c.inc(1.6)  # Increment by given value
     '''
     _type = 'counter'
     _reserved_labelnames = []
@@ -428,38 +401,19 @@ class Gauge(object):
     '''Gauge metric, to report instantaneous values.
 
      Examples of Gauges include:
-        - Inprogress requests
-        - Number of items in a queue
-        - Free memory
-        - Total memory
-        - Temperature
+        Inprogress requests
+        Number of items in a queue
+        Free memory
+        Total memory
+        Temperature
 
      Gauges can go both up and down.
 
         from prometheus_client import Gauge
-
         g = Gauge('my_inprogress_requests', 'Description of gauge')
         g.inc()      # Increment by 1
         g.dec(10)    # Decrement by given value
         g.set(4.2)   # Set to a given value
-
-     There are utilities for common use cases:
-
-        g.set_to_current_time()   # Set to current unixtime
-
-        # Increment when entered, decrement when exited.
-        @g.track_inprogress()
-        def f():
-            pass
-
-        with g.track_inprogress():
-            pass
-
-     A Gauge can also take its value from a callback:
-
-        d = Gauge('data_objects', 'Number of objects')
-        my_dict = {}
-        d.set_function(lambda: len(my_dict))
     '''
     _type = 'gauge'
     _reserved_labelnames = []
@@ -540,7 +494,8 @@ class Gauge(object):
         '''Call the provided function to return the Gauge value.
 
         The function must return a float, and may be called from
-        multiple threads. All other methods of the Gauge become NOOPs.
+        multiple threads.
+        All other methods of the Gauge become NOOPs.
         '''
         def samples(self):
             return (('', {}, float(f())), )
@@ -560,26 +515,19 @@ class Summary(object):
 
     Example for a Summary:
 
-        from prometheus_client import Summary
-
-        s = Summary('request_size_bytes', 'Request size (bytes)')
-        s.observe(512)  # Observe 512 (bytes)
+    from prometheus_client import Summary
+    s = Summary('request_size_bytes', 'Request size (bytes)')
+    s.observe(512)  # Observe 512 (bytes)
 
     Example for a Summary using time:
+    from prometheus_client import Summary
+    REQUEST_TIME = Summary('response_latency_seconds', 'Response latency (seconds)')
 
-        from prometheus_client import Summary
-
-        REQUEST_TIME = Summary('response_latency_seconds', 'Response latency (seconds)')
-
-        @REQUEST_TIME.time()
-        def create_response(request):
-          """A dummy function"""
-          time.sleep(1)
+    @REQUEST_TIME.time()
+    def create_response(request):
+      """A dummy function"""
+      time.sleep(1)
 
-    Example for using the same Summary object as a context manager:
-
-        with REQUEST_TIME.time():
-            pass  # Logic to be timed
     '''
     _type = 'summary'
     _reserved_labelnames = ['quantile']
@@ -648,31 +596,22 @@ class Histogram(object):
 
     Example for a Histogram:
 
-        from prometheus_client import Histogram
+    from prometheus_client import Histogram
+    h = Histogram('request_size_bytes', 'Request size (bytes)')
+    h.observe(512)  # Observe 512 (bytes)
 
-        h = Histogram('request_size_bytes', 'Request size (bytes)')
-        h.observe(512)  # Observe 512 (bytes)
 
     Example for a Histogram using time:
+    from prometheus_client import Histogram
+    REQUEST_TIME = Histogram('response_latency_seconds', 'Response latency (seconds)')
 
-        from prometheus_client import Histogram
-
-        REQUEST_TIME = Histogram('response_latency_seconds', 'Response latency (seconds)')
-
-        @REQUEST_TIME.time()
-        def create_response(request):
-          """A dummy function"""
-          time.sleep(1)
-
-    Example of using the same Histogram object as a context manager:
-
-        with REQUEST_TIME.time():
-            pass  # Logic to be timed
+    @REQUEST_TIME.time()
+    def create_response(request):
+      """A dummy function"""
+      time.sleep(1)
 
     The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
     They can be overridden by passing `buckets` keyword argument to `Histogram`.
-
-    **NB** The Python client doesn't store or expose quantile information at this time.
     '''
     _type = 'histogram'
     _reserved_labelnames = ['histogram']
diff --git a/prometheus_client/twisted/__init__.py b/prometheus_client/twisted/__init__.py
new file mode 100644
index 0000000..87e0b8a
--- /dev/null
+++ b/prometheus_client/twisted/__init__.py
@@ -0,0 +1,3 @@
+from ._exposition import MetricsResource
+
+__all__ = ['MetricsResource']
diff --git a/prometheus_client/twisted/_exposition.py b/prometheus_client/twisted/_exposition.py
new file mode 100644
index 0000000..66c548c
--- /dev/null
+++ b/prometheus_client/twisted/_exposition.py
@@ -0,0 +1,18 @@
+from __future__ import absolute_import, unicode_literals
+from .. import REGISTRY, generate_latest, CONTENT_TYPE_LATEST
+
+from twisted.web.resource import Resource
+
+
+class MetricsResource(Resource):
+    """
+    Twisted ``Resource`` that serves prometheus metrics.
+    """
+    isLeaf = True
+
+    def __init__(self, registry=REGISTRY):
+        self.registry = registry
+
+    def render_GET(self, request):
+        request.setHeader(b'Content-Type', CONTENT_TYPE_LATEST.encode('ascii'))
+        return generate_latest(self.registry)
diff --git a/setup.py b/setup.py
index 14f3489..de535fb 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup
 
 setup(
     name = "prometheus_client",
-    version = "0.0.13",
+    version = "0.0.12",
     author = "Brian Brazil",
     author_email = "brian.brazil@robustperception.io",
     description = ("Python client for the Prometheus monitoring system."),
@@ -11,7 +11,10 @@ setup(
     license = "Apache Software License 2.0",
     keywords = "prometheus monitoring instrumentation client",
     url = "https://github.com/prometheus/client_python",
-    packages=['prometheus_client', 'prometheus_client.bridge'],
+    packages=['prometheus_client', 'prometheus_client.bridge', 'prometheus_client.twisted'],
+    extras_requires={
+        'twisted': ['twisted'],
+    },
     test_suite="tests",
     classifiers=[
         "Development Status :: 4 - Beta",


Test patch:
diff --git a/tests/test_exposition.py b/tests/test_exposition.py
index be555f9..c5fd1fe 100644
--- a/tests/test_exposition.py
+++ b/tests/test_exposition.py
@@ -97,7 +97,6 @@ class TestPushGateway(unittest.TestCase):
                 self.send_response(201)
                 length = int(self.headers['content-length'])
                 requests.append((self, self.rfile.read(length)))
-                self.end_headers()
 
             do_POST = do_PUT
             do_DELETE = do_PUT
diff --git a/tests/test_twisted.py b/tests/test_twisted.py
new file mode 100644
index 0000000..8554cc9
--- /dev/null
+++ b/tests/test_twisted.py
@@ -0,0 +1,48 @@
+from __future__ import absolute_import, unicode_literals
+
+from unittest import skipUnless
+
+from prometheus_client import Counter
+from prometheus_client import CollectorRegistry, generate_latest
+
+try:
+    from prometheus_client.twisted import MetricsResource
+
+    from twisted.trial.unittest import TestCase
+    from twisted.web.server import Site
+    from twisted.web.resource import Resource
+    from twisted.internet import reactor
+    from twisted.web.client import Agent
+    from twisted.web.client import readBody
+    HAVE_TWISTED = True
+except ImportError:
+    from unittest import TestCase
+    HAVE_TWISTED = False
+
+
+class MetricsResourceTest(TestCase):
+    @skipUnless(HAVE_TWISTED, "Don't have twisted installed.")
+    def setUp(self):
+        self.registry = CollectorRegistry()
+
+    def test_reports_metrics(self):
+        """
+        ``MetricsResource`` serves the metrics from the provided registry.
+        """
+        c = Counter('cc', 'A counter', registry=self.registry)
+        c.inc()
+
+        root = Resource()
+        root.putChild(b'metrics', MetricsResource(registry=self.registry))
+        server = reactor.listenTCP(0, Site(root))
+        self.addCleanup(server.stopListening)
+
+        agent = Agent(reactor)
+        port = server.getHost().port
+        url = u"http://localhost:{port}/metrics".format(port=port)
+        d = agent.request(b"GET", url.encode("ascii"))
+
+        d.addCallback(readBody)
+        d.addCallback(self.assertEqual, generate_latest(self.registry))
+
+        return d