From 4b44d460f1e19e4883dcf24cb1d707d11086032b Mon Sep 17 00:00:00 2001 From: Younes Oumakhou Date: Wed, 27 Dec 2023 14:54:12 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20prometheus=20service?= =?UTF-8?q?=20availability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metricheq/deducers/prometheus.py | 65 ++++++++++++++++ tests/prometheus/test_availability_deducer.py | 78 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 metricheq/deducers/prometheus.py create mode 100644 tests/prometheus/test_availability_deducer.py diff --git a/metricheq/deducers/prometheus.py b/metricheq/deducers/prometheus.py new file mode 100644 index 0000000..1d28cc7 --- /dev/null +++ b/metricheq/deducers/prometheus.py @@ -0,0 +1,65 @@ +from datetime import datetime +from typing import Optional +from pydantic import BaseModel +from metricheq.connectors.base import Connector +from metricheq.connectors.prometheus import PrometheusConnector +from urllib.parse import quote + +from metricheq.deducers.base import Deducer + +# TODO: provide default value for step based on start, end date and points + + +class PrometheusServiceAvailabilityParams(BaseModel): + labels: dict[str, str] + start_time: datetime + end_time: datetime + step: Optional[int] = None + + +class PrometheusServiceAvailabilityDeducer(Deducer): + def __init__(self, connector: Connector, params: dict): + if not isinstance(connector, PrometheusConnector): + raise TypeError( + "The provided connector is not a valid prometheus connector" + ) + self.params_model = PrometheusServiceAvailabilityParams(**params) + super().__init__(connector, params) + + def retrieve_data(self): + label_filters = ",".join( + [f'{key}="{value}"' for key, value in self.params_model.labels.items()] + ) + start_date_rfc3339 = self.params_model.start_time.isoformat() + "Z" + end_date_rfc3339 = self.params_model.end_time.isoformat() + "Z" + step = self.params.get("step", 60) + + query = f"up{{{label_filters}}}" + encoded_query = quote(query) + endpoint = f"query_range?query={encoded_query}&start={start_date_rfc3339}&end={end_date_rfc3339}&step={step}" + + response = self.client.make_request(endpoint) + if response.status_code == 200: + return response.json() + else: + response.raise_for_status() + + def process_data(self, data): + results = data.get("data", {}).get("result", []) + + up_times = 0 + total_times = 0 + + for result in results: + for _, status in result.get("values", []): + total_times += 1 + if status == "1": # service was up + up_times += 1 + + availability_percentage = ( + (up_times / total_times) * 100 if total_times > 0 else 0 + ) + return availability_percentage + + def finalize(self, processed_data): + return processed_data diff --git a/tests/prometheus/test_availability_deducer.py b/tests/prometheus/test_availability_deducer.py new file mode 100644 index 0000000..bddb0f0 --- /dev/null +++ b/tests/prometheus/test_availability_deducer.py @@ -0,0 +1,78 @@ +from datetime import datetime, timedelta +import unittest +from unittest.mock import Mock + +from requests import HTTPError + +from metricheq.connectors.prometheus import PrometheusConnector +from metricheq.deducers.prometheus import PrometheusServiceAvailabilityDeducer + + +class TestPrometheusServiceAvailabilityDeducer(unittest.TestCase): + def setUp(self): + self.mock_client = Mock() + self.mock_connector = Mock(spec=PrometheusConnector, client=self.mock_client) + + start_time = datetime.now() - timedelta(days=1) + end_time = datetime.now() + + self.params = { + "labels": {"job": "job_label"}, + "start_time": start_time, + "end_time": end_time, + "step": 60, + } + + self.deducer = PrometheusServiceAvailabilityDeducer( + self.mock_connector, self.params + ) + + def test_init_with_invalid_connector(self): + with self.assertRaises(TypeError): + invalid_connector = Mock() + PrometheusServiceAvailabilityDeducer(invalid_connector, self.params) + + def test_retrieve_data_successful(self): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"data": {"result": []}} + self.mock_client.make_request.return_value = mock_response + + result = self.deducer.retrieve_data() + self.assertIsNotNone(result) + + def test_retrieve_data_failure(self): + mock_response = Mock() + mock_response.status_code = 500 + mock_response.raise_for_status.side_effect = HTTPError("500 Server Error") + self.mock_client.make_request.return_value = mock_response + + with self.assertRaises(HTTPError): + self.deducer.retrieve_data() + + def test_process_data(self): + mock_data = { + "data": {"result": [{"values": [["timestamp", "1"], ["timestamp", "0"]]}]} + } + result = self.deducer.process_data(mock_data) + expected_availability = ( + 50.0 # Assuming one 'up' and one 'down' in the mock data + ) + self.assertEqual(result, expected_availability) + + def test_finalize(self): + processed_data = 75.0 + result = self.deducer.finalize(processed_data) + self.assertEqual(result, processed_data) + + def test_deduce_integration(self): + mock_retrieve_data = Mock(return_value={"data": {"result": []}}) + mock_process_data = Mock(return_value=100.0) + mock_finalize = Mock(return_value=100.0) + + self.deducer.retrieve_data = mock_retrieve_data + self.deducer.process_data = mock_process_data + self.deducer.finalize = mock_finalize + + result = self.deducer.deduce() + self.assertEqual(result, 100.0)