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