Skip to content

Commit 3d17b4f

Browse files
author
piptouque
committed
Add support for operator in labels
This allows us to match labels with regular expressions.
1 parent 1c260f2 commit 3d17b4f

File tree

3 files changed

+96
-17
lines changed

3 files changed

+96
-17
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from collections.abc import Sequence
2+
from typing import Dict,Tuple,Union,Optional
3+
from enum import StrEnum
4+
from functools import reduce
5+
import logging
6+
7+
class LabelQueryOp(StrEnum):
8+
EQUAL='='
9+
NOT_EQUAL='!='
10+
REGEX_EQUAL='=~'
11+
REGEX_NOT_EQUAL='!~'
12+
13+
LabelQuery=Union[str,Tuple[LabelQueryOp, str]]
14+
MetricLabelQuery = Dict[str,LabelQuery]
15+
16+
def query_to_str(metric_name: str, label_query: Optional[MetricLabelQuery]=None)->str:
17+
"""
18+
Contruct query string from label query dictionary
19+
20+
:param label_query: (MetricLabelQuery) The label query dictionary. Default is None
21+
:return: (str) Query string inside brackets
22+
:raises:
23+
(ValueError) Raises an exception in case of an invalid label query operator
24+
"""
25+
if not label_query:
26+
return metric_name
27+
def _format_label_query(label_key: str, label: LabelQuery)->str:
28+
if isinstance(label, Sequence) and not isinstance(label, str):
29+
if len(label) != 2:
30+
raise ValueError(f"wrong number of elements in label query with operator: {len(label)} instead of 2")
31+
label_op=label[0]
32+
if label_op not in LabelQueryOp:
33+
raise ValueError(f"unknown label operator: '{label_op}'")
34+
label_value=label[1]
35+
return f"{label_key}{label_op}'{label_value}'"
36+
else:
37+
return f"{label_key}{LabelQueryOp.EQUAL}'{label}'"
38+
label_list=[_format_label_query(label_key, label) for label_key, label in label_query.items()]
39+
return metric_name + "{" + ",".join(label_list) + "}"

prometheus_api_client/prometheus_connect.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from requests.packages.urllib3.util.retry import Retry
1111
from requests import Session
1212

13+
from .metric_query import MetricLabelQuery, query_to_str
1314
from .exceptions import PrometheusApiClientException
1415

1516
# set up logging
@@ -229,14 +230,14 @@ def get_label_values(self, label_name: str, params: dict = None):
229230
return labels
230231

231232
def get_current_metric_value(
232-
self, metric_name: str, label_config: dict = None, params: dict = None
233+
self, metric_name: str, label_config: MetricLabelQuery = None, params: dict = None
233234
):
234235
r"""
235236
Get the current metric value for the specified metric and label configuration.
236237

237238
:param metric_name: (str) The name of the metric
238-
:param label_config: (dict) A dictionary that specifies metric labels and their
239-
values
239+
:param label_config: (MetricLabelQuery) A dictionary specifying metric labels and their
240+
values, with optional operator (default is equality).
240241
:param params: (dict) Optional dictionary containing GET parameters to be sent
241242
along with the API request, such as "time"
242243
:returns: (list) A list of current metric values for the specified metric
@@ -249,17 +250,13 @@ def get_current_metric_value(
249250

250251
prom = PrometheusConnect()
251252

252-
my_label_config = {'cluster': 'my_cluster_id', 'label_2': 'label_2_value'}
253+
my_label_config = {'cluster': 'my_cluster_id', 'label_2': ('=~','label_2_.*')}
253254

254255
prom.get_current_metric_value(metric_name='up', label_config=my_label_config)
255256
"""
256257
params = params or {}
257258
data = []
258-
if label_config:
259-
label_list = [str(key + "=" + "'" + label_config[key] + "'") for key in label_config]
260-
query = metric_name + "{" + ",".join(label_list) + "}"
261-
else:
262-
query = metric_name
259+
query = query_to_str(metric_name, label_query=label_config)
263260

264261
# using the query API to get raw data
265262
response = self._session.request(
@@ -284,7 +281,7 @@ def get_current_metric_value(
284281
def get_metric_range_data(
285282
self,
286283
metric_name: str,
287-
label_config: dict = None,
284+
label_config: MetricLabelQuery = None,
288285
start_time: datetime = (datetime.now() - timedelta(minutes=10)),
289286
end_time: datetime = datetime.now(),
290287
chunk_size: timedelta = None,
@@ -295,8 +292,8 @@ def get_metric_range_data(
295292
Get the current metric value for the specified metric and label configuration.
296293

297294
:param metric_name: (str) The name of the metric.
298-
:param label_config: (dict) A dictionary specifying metric labels and their
299-
values.
295+
:param label_config: (MetricLabelQuery) A dictionary specifying metric labels and their
296+
values, with optional operator (default is equality).
300297
:param start_time: (datetime) A datetime object that specifies the metric range start time.
301298
:param end_time: (datetime) A datetime object that specifies the metric range end time.
302299
:param chunk_size: (timedelta) Duration of metric data downloaded in one request. For
@@ -338,11 +335,7 @@ def get_metric_range_data(
338335
raise ValueError("specified chunk_size is too big")
339336
chunk_seconds = round(chunk_size.total_seconds())
340337

341-
if label_config:
342-
label_list = [str(key + "=" + "'" + label_config[key] + "'") for key in label_config]
343-
query = metric_name + "{" + ",".join(label_list) + "}"
344-
else:
345-
query = metric_name
338+
query = query_to_str(metric_name, label_query=label_config)
346339
_LOGGER.debug("Prometheus Query: %s", query)
347340

348341
while start < end:

tests/test_metric_query.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Test module for class PrometheusConnect."""
2+
import unittest
3+
4+
from prometheus_api_client.metric_query import query_to_str
5+
6+
class TestMetricQuery(unittest.TestCase):
7+
"""Test module for metric query."""
8+
9+
def test_query_to_str_with_wrong_label_query(self): # noqa D102
10+
# wrong op ('~=' instead of '=~')
11+
with self.assertRaises(ValueError, msg=f"unknown label operator: '~='"):
12+
_ = query_to_str(
13+
metric_name="up",
14+
label_query={"some_label": ("~=", "some-value-.*")}
15+
)
16+
# inverted label value and op
17+
with self.assertRaises(ValueError, msg=f"unknown label operator: 'some-value-.*'"):
18+
_ = query_to_str(
19+
metric_name="up",
20+
label_query={"some_label": ("some-value-.*", "=~")}
21+
)
22+
# Wrong number of label query arguments
23+
with self.assertRaises(ValueError, msg=f"wrong number of elements in label query with operator: 3 instead of 2"):
24+
_ = query_to_str(
25+
metric_name="up",
26+
label_query={"some_label": ("=~", "some-value-.*", "whatever")}
27+
)
28+
def test_query_to_str_with_correct_label_query(self): # noqa D102
29+
correct_label_queries = [
30+
{ "some_label": "some-value"}, # exact match
31+
{ "some_label": ("=", "some-value")}, # exact match, explicit op
32+
{ "some_label": ("!=", "some-value")}, # negative match
33+
{ "some_label": ("=~", "some-value-.*")}, # regex match
34+
{ "some_label": ("!~", "some-value-.*")}, # negative regex match
35+
]
36+
for label_query in correct_label_queries:
37+
try:
38+
_ = query_to_str(
39+
metric_name="up",
40+
label_query=label_query
41+
)
42+
except Exception as e:
43+
self.fail(f"query_to_str('up') with label_config raised an unexpected exception: {e}")
44+
45+
46+
if __name__ == "__main__":
47+
unittest.main()

0 commit comments

Comments
 (0)