Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: add prometheus service availability #7

Merged
merged 1 commit into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions metricheq/deducers/prometheus.py
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions tests/prometheus/test_availability_deducer.py
Original file line number Diff line number Diff line change
@@ -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)