diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b18e311d68dba3d86032018f6d6fd1d3d3f8b86..8ec660be0046a3a41520f677f13fce1c331ff7ba 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 b991fa6d80ef2ae304d432c063b88bd398f0ef89..e569c69906a57af665c99427feebcfa9e3aad337 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 a96e660f282f03adefe8f59aaed245149625e7ff..8f4d91e57cb558462af733e8c96cf8d8cd4513f5 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 06424dd3afa3020dfcfac5d2bca1fae65223710b..fdd46e0861c20ecbb2435d47c4b031212e408620 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 1af59f45139dbeb5165b8840095788c2ddcdbb0f..5cb27db688e65943a0ff3831c5caa64ba17788cc 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 357a47428ce944341b604cbf12caf097ead26211..5c7f901b05e6d239e29ea739c9f3d87cb9a9d5bf 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 0499463b3c569bf6cfae425432a8d29f3ef288ca..aa5ac43a3c5a8b982435b3749c4cae4728a26a37 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 816e524bda3e066d46c0f7f05fca59284f7f3440..af9ec7e09c9d7c4cc0128a2287473e58965b78a2 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)