Files
python-observabilityclient/observabilityclient/tests/unit/test_prometheus_client.py
Jaromir Wysoglad ccf4ace2b5 Enable providing keystone session for Prometheus
This enables us to create a PrometheusAPIClient, which uses a
keystone session when communicating with Prometheus, which
causes the requests to include an X-Auth-Token header
with the user's keystone token. This will enable observabilityclient
to authenticate when communicating with Aetos in the future.

Change-Id: I3693a6906efccdbb193ddd1e927ed83975592442
2025-04-17 10:57:57 -04:00

517 lines
18 KiB
Python

# Copyright 2023 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
import requests
import testtools
from observabilityclient import prometheus_client as client
class MetricListMatcher(testtools.Matcher):
def __init__(self, expected):
self.expected = expected
def __str__(self):
return ("Matches Lists of metrics as returned "
"by prometheus_client.PremetheusAPIClient.query")
def metric_to_str(self, metric):
return (f"Labels: {metric.labels}\n"
f"Timestamp: {metric.timestamp}\n"
f"Value: {metric.value}")
def match(self, observed):
if len(self.expected) != len(observed):
description = (f"len(expected) != len(observed) because "
f"{len(self.expected)} != {len(observed)}")
return testtools.matchers.Mismatch(description=description)
for e in self.expected:
for o in observed:
if (e.timestamp == o.timestamp and
e.value == o.value and
e.labels == o.labels):
observed.remove(o)
break
if len(observed) != 0:
description = "Couldn't match the following metrics:\n"
for o in observed:
description += self.metric_to_str(o) + "\n\n"
return testtools.matchers.Mismatch(description=description)
return None
class PrometheusAPIClientTestBase(testtools.TestCase):
def setUp(self):
super(PrometheusAPIClientTestBase, self).setUp()
class GoodResponse(object):
def __init__(self):
self.status_code = 200
def json(self):
return {"status": "success"}
class BadResponse(object):
def __init__(self):
self.status_code = 500
def json(self):
return {"status": "error", "error": "test_error"}
class NoContentResponse(object):
def __init__(self):
self.status_code = 204
def json(self):
raise requests.exceptions.JSONDecodeError("No content")
class PrometheusAPIClientTest(PrometheusAPIClientTestBase):
def test_get(self):
url = "test"
expected_url = "http://localhost:9090/api/v1/test"
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
expected_params = params
return_value = self.GoodResponse()
with mock.patch.object(requests.Session, 'get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
c._get(url, params)
m.assert_called_with(expected_url,
params=expected_params,
headers={'Accept': 'application/json',
'Accept-Encoding': 'identity'})
def test_get_error(self):
url = "test"
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
return_value = self.BadResponse()
with mock.patch.object(requests.Session, 'get',
return_value=return_value):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c._get, url, params)
return_value = self.NoContentResponse()
with mock.patch.object(requests.Session, 'get',
return_value=return_value):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c._get, url, params)
def test_post(self):
url = "test"
expected_url = "http://localhost:9090/api/v1/test"
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
expected_params = params
return_value = self.GoodResponse()
with mock.patch.object(requests.Session, 'post',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
c._post(url, params)
m.assert_called_with(expected_url,
params=expected_params,
headers={'Accept': 'application/json'})
def test_post_error(self):
url = "test"
params = {"query": "ceilometer_image_size{publisher='localhost'}"}
return_value = self.BadResponse()
with mock.patch.object(requests.Session, 'post',
return_value=return_value):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c._post, url, params)
return_value = self.NoContentResponse()
with mock.patch.object(requests.Session, 'post',
return_value=return_value):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c._post, url, params)
class PrometheusAPIClientQueryTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodQueryResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.result1 = {
"metric": {
"__name__": "test1",
},
"value": [103254, "1"]
}
self.result2 = {
"metric": {
"__name__": "test2",
},
"value": [103255, "2"]
}
self.expected = [client.PrometheusMetric(self.result1),
client.PrometheusMetric(self.result2)]
def json(self):
return {
"status": "success",
"data": {
"resultType": "vector",
"result": [self.result1, self.result2]
}
}
class EmptyQueryResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.expected = []
def json(self):
return {
"status": "success",
"data": {
"resultType": "vector",
"result": []
}
}
def test_query(self):
query = "ceilometer_image_size{publisher='localhost.localdomain'}"
matcher = MetricListMatcher(self.GoodQueryResponse().expected)
return_value = self.GoodQueryResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.query(query)
m.assert_called_with("query", {"query": query})
self.assertThat(ret, matcher)
return_value = self.EmptyQueryResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.query(query)
self.assertEqual(self.EmptyQueryResponse().expected, ret)
def test_query_error(self):
query = "ceilometer_image_size{publisher='localhost.localdomain'}"
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_get',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError, c.query, query)
class PrometheusAPIClientSeriesTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodSeriesResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.data = [{
"__name__": "up",
"job": "prometheus",
"instance": "localhost:9090"
}, {
"__name__": "up",
"job": "node",
"instance": "localhost:9091"
}, {
"__name__": "process_start_time_seconds",
"job": "prometheus",
"instance": "localhost:9090"
}]
self.expected = self.data
def json(self):
return {
"status": "success",
"data": self.data
}
class EmptySeriesResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.data = []
self.expected = self.data
def json(self):
return {
"status": "success",
"data": self.data
}
def test_series(self):
matches = ["up", "ceilometer_image_size"]
return_value = self.GoodSeriesResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.series(matches)
m.assert_called_with("series", {"match[]": matches})
self.assertEqual(ret, self.GoodSeriesResponse().data)
return_value = self.EmptySeriesResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.series(matches)
m.assert_called_with("series", {"match[]": matches})
self.assertEqual(ret, self.EmptySeriesResponse().data)
def test_series_error(self):
matches = ["up", "ceilometer_image_size"]
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_get',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c.series,
matches)
class PrometheusAPIClientLabelsTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodLabelsResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.labels = ["up", "job", "project_id"]
def json(self):
return {
"status": "success",
"data": self.labels
}
def test_labels(self):
return_value = self.GoodLabelsResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.labels()
m.assert_called_with("labels")
self.assertEqual(ret, self.GoodLabelsResponse().labels)
def test_labels_error(self):
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_get',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError, c.labels)
class PrometheusAPIClientLabelValuesTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodLabelValuesResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.values = ["prometheus", "some_other_value"]
def json(self):
return {
"status": "success",
"data": self.values
}
class EmptyLabelValuesResponse(PrometheusAPIClientTestBase.GoodResponse):
def __init__(self):
super().__init__()
self.values = []
def json(self):
return {
"status": "success",
"data": self.values
}
def test_label_values(self):
label_name = "job"
return_value = self.GoodLabelValuesResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.label_values(label_name)
m.assert_called_with(f"label/{label_name}/values")
self.assertEqual(ret, self.GoodLabelValuesResponse().values)
return_value = self.EmptyLabelValuesResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_get',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.label_values(label_name)
m.assert_called_with(f"label/{label_name}/values")
self.assertEqual(ret, self.EmptyLabelValuesResponse().values)
def test_label_values_error(self):
label_name = "job"
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_get',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c.label_values,
label_name)
class PrometheusAPIClientDeleteTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodDeleteResponse(PrometheusAPIClientTestBase.NoContentResponse):
pass
def test_delete(self):
matches = ["{job='prometheus'}", "up"]
start = 1
end = 12
resp = self.GoodDeleteResponse()
post_exception = client.PrometheusAPIClientError(resp)
with mock.patch.object(client.PrometheusAPIClient, '_post',
side_effect=post_exception) as m:
c = client.PrometheusAPIClient("localhost:9090")
# _post is expected to raise an exception. It's expected
# that the exception is caught inside delete. This
# test should run without exception getting out of delete
try:
c.delete(matches, start, end)
except Exception as ex: # noqa: B902
self.fail("Exception risen by delete: " + ex)
m.assert_called_with("admin/tsdb/delete_series",
{"match[]": matches,
"start": start,
"end": end})
def test_delete_error(self):
matches = ["{job='prometheus'}", "up"]
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_post',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c.delete,
matches)
class PrometheusAPIClientCleanTombstonesTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodCleanTombResponse(PrometheusAPIClientTestBase.NoContentResponse):
pass
def test_clean_tombstones(self):
resp = self.GoodCleanTombResponse()
post_exception = client.PrometheusAPIClientError(resp)
with mock.patch.object(client.PrometheusAPIClient, '_post',
side_effect=post_exception) as m:
c = client.PrometheusAPIClient("localhost:9090")
# _post is expected to raise an exception. It's expected
# that the exception is caught inside clean_tombstones. This
# test should run without exception getting out of clean_tombstones
try:
c.clean_tombstones()
except Exception as ex: # noqa: B902
self.fail("Exception risen by clean_tombstones: " + ex)
m.assert_called_with("admin/tsdb/clean_tombstones")
def test_snapshot_error(self):
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_post',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c.clean_tombstones)
class PrometheusAPIClientSnapshotTest(PrometheusAPIClientTestBase):
def setUp(self):
super().setUp()
class GoodSnapshotResponse(PrometheusAPIClientTestBase.NoContentResponse):
def __init__(self):
super().__init__()
self.filename = "somefilename"
def json(self):
return {
"status": "success",
"data": {
"name": self.filename
}
}
def test_snapshot(self):
return_value = self.GoodSnapshotResponse().json()
with mock.patch.object(client.PrometheusAPIClient, '_post',
return_value=return_value) as m:
c = client.PrometheusAPIClient("localhost:9090")
ret = c.snapshot()
m.assert_called_with("admin/tsdb/snapshot")
self.assertEqual(ret, self.GoodSnapshotResponse().filename)
def test_snapshot_error(self):
client_exception = client.PrometheusAPIClientError(self.BadResponse())
with mock.patch.object(client.PrometheusAPIClient, '_post',
side_effect=client_exception):
c = client.PrometheusAPIClient("localhost:9090")
self.assertRaises(client.PrometheusAPIClientError,
c.snapshot)