From c674cc4a0df06f0beb427509cf4ddf4370745eb5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Fri, 7 Feb 2020 15:12:19 +0100
Subject: [PATCH] added a http exporter thread

---
 CHANGELOG.md                                  |  2 +-
 docs/instrumentation/metrics.rst              |  8 +++-
 satella/__init__.py                           |  2 +-
 .../metrics/exporters/__init__.py             |  4 +-
 .../metrics/exporters/prometheus.py           | 42 +++++++++++++++++--
 .../metrics/metric_types/counter.py           |  3 +-
 setup.py                                      |  2 +-
 .../test_metrics/test_exporters.py            | 13 +++++-
 8 files changed, 63 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b18e311..8ec660be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
 # v2.4.4
 
-* _TBA_
+* added `PrometheusHTTPExporterThread`
 
 # v2.4.3
 
diff --git a/docs/instrumentation/metrics.rst b/docs/instrumentation/metrics.rst
index b991fa6d..e569c699 100644
--- a/docs/instrumentation/metrics.rst
+++ b/docs/instrumentation/metrics.rst
@@ -159,4 +159,10 @@ For example in such a way:
         metric = getMetric()
         return metric_data_collection_to_prometheus(metric.to_metric_data())
 
-Dots in metric names will be replaced with underscores.
\ No newline at end of file
+Dots in metric names will be replaced with underscores.
+
+Or, if you need a HTTP server that will export metrics for Prometheus, use this class
+that is a daemonic thread you can use to easily expose metrics to Prometheus:
+
+.. autoclass:: satella.instrumentation.metrics.exporters.PrometheusHTTPExporterThread
+    :members:
diff --git a/satella/__init__.py b/satella/__init__.py
index a96e660f..8f4d91e5 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.4.4rc1'
+__version__ = '2.4.4rc2'
diff --git a/satella/instrumentation/metrics/exporters/__init__.py b/satella/instrumentation/metrics/exporters/__init__.py
index 06424dd3..fdd46e08 100644
--- a/satella/instrumentation/metrics/exporters/__init__.py
+++ b/satella/instrumentation/metrics/exporters/__init__.py
@@ -1,3 +1,3 @@
-from .prometheus import metric_data_collection_to_prometheus
+from .prometheus import metric_data_collection_to_prometheus, PrometheusHTTPExporterThread
 
-__all__ = ['metric_data_collection_to_prometheus']
+__all__ = ['metric_data_collection_to_prometheus', 'PrometheusHTTPExporterThread']
diff --git a/satella/instrumentation/metrics/exporters/prometheus.py b/satella/instrumentation/metrics/exporters/prometheus.py
index 1af59f45..5cb27db6 100644
--- a/satella/instrumentation/metrics/exporters/prometheus.py
+++ b/satella/instrumentation/metrics/exporters/prometheus.py
@@ -1,13 +1,47 @@
 import logging
 import io
-import copy
-from satella.coding import for_argument
-
+import threading
+import http.server
+from .. import getMetric
 from ..data import MetricData, MetricDataCollection
 
 logger = logging.getLogger(__name__)
 
-__all__ = ['metric_data_collection_to_prometheus']
+__all__ = ['metric_data_collection_to_prometheus', 'PrometheusHTTPExporterThread']
+
+
+class PrometheusHandler(http.server.BaseHTTPRequestHandler):
+
+    def do_GET(self):
+        if self.path != '/metrics':
+            self.send_error(404, 'Unknown path. Only /metrics is supported.')
+            return
+
+        root_metric = getMetric()
+
+        metric_data = metric_data_collection_to_prometheus(root_metric.to_metric_data())
+        self.send_response(200)
+        self.send_header('Content-Type', 'text/plain; charset=utf-8')
+        self.end_headers()
+        self.wfile.write(metric_data.encode('utf8'))
+
+
+class PrometheusHTTPExporterThread(threading.Thread):
+    """
+    A daemon thread that listens on given interface as a HTTP server, ready to serve as a connection
+    point for Prometheus to scrape metrics off this service.
+
+    :param interface: a interface to bind to
+    :param port: a port to bind to
+    """
+    def __init__(self, interface: str, port: int):
+        super().__init__(daemon=True)
+        self.interface = interface
+        self.port = port
+
+    def run(self):
+        with http.server.HTTPServer((self.interface, self.port), PrometheusHandler) as httpd:
+            httpd.serve_forever()
 
 
 class RendererObject(io.StringIO):
diff --git a/satella/instrumentation/metrics/metric_types/counter.py b/satella/instrumentation/metrics/metric_types/counter.py
index 357a4742..5c7f901b 100644
--- a/satella/instrumentation/metrics/metric_types/counter.py
+++ b/satella/instrumentation/metrics/metric_types/counter.py
@@ -1,6 +1,5 @@
 import logging
-import typing as tp
-from .base import EmbeddedSubmetrics, LeafMetric
+from .base import EmbeddedSubmetrics
 from ..data import  MetricData, MetricDataCollection
 from .registry import register_metric
 logger = logging.getLogger(__name__)
diff --git a/setup.py b/setup.py
index 0499463b..aa5ac43a 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ setup(keywords=['ha', 'high availability', 'scalable', 'scalability', 'server'],
             'psutil'
       ],
       tests_require=[
-          "nose2", "mock", "coverage", "nose2[coverage_plugin]"
+          "nose2", "mock", "coverage", "nose2[coverage_plugin]", "requests"
       ],
       test_suite='nose2.collector.collector',
       python_requires='!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*',
diff --git a/tests/test_instrumentation/test_metrics/test_exporters.py b/tests/test_instrumentation/test_metrics/test_exporters.py
index 816e524b..af9ec7e0 100644
--- a/tests/test_instrumentation/test_metrics/test_exporters.py
+++ b/tests/test_instrumentation/test_metrics/test_exporters.py
@@ -1,7 +1,10 @@
 import unittest
 import typing as tp
 import logging
-from satella.instrumentation.metrics.exporters import metric_data_collection_to_prometheus
+import requests
+from satella.instrumentation.metrics import getMetric
+from satella.instrumentation.metrics.exporters import metric_data_collection_to_prometheus, \
+    PrometheusHTTPExporterThread
 from satella.instrumentation.metrics import MetricData, MetricDataCollection
 logger = logging.getLogger(__name__)
 
@@ -16,3 +19,11 @@ class TestExporters(unittest.TestCase):
                                   MetricData('root.metric', 6, {'k': 4})])
         b = metric_data_collection_to_prometheus(a)
         self.assertIn("""root_metric{k="4"} 6""", b)
+
+    def test_exporter_http_server(self):
+        phet = PrometheusHTTPExporterThread('localhost', 1025)
+        phet.start()
+        metr = getMetric('test.metric', 'int')
+        metr.runtime(5)
+        data = requests.get('http://localhost:1025/metrics')
+        self.assertIn('test_metric 5', data.text)
-- 
GitLab