Skip to content

Commit 8325bea

Browse files
authored
Merge pull request #34 from 4n4nd/v0.0.2
Remove dateparser as a dependency and use datetime objects in PrometheusConnect
2 parents 2972f21 + 0b40014 commit 8325bea

File tree

3 files changed

+153
-36
lines changed

3 files changed

+153
-36
lines changed

prometheus_api_client/prometheus_connect.py

+36-36
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import sys
88
import json
99
import logging
10+
from datetime import datetime, timedelta
1011
import requests
11-
import dateparser
1212
from retrying import retry
1313

1414
# set up logging
@@ -97,7 +97,6 @@ def get_current_metric_value(
9797
data = []
9898
if label_config:
9999
label_list = [str(key + "=" + "'" + label_config[key] + "'") for key in label_config]
100-
# print(label_list)
101100
query = metric_name + "{" + ",".join(label_list) + "}"
102101
else:
103102
query = metric_name
@@ -123,9 +122,9 @@ def get_metric_range_data(
123122
self,
124123
metric_name: str,
125124
label_config: dict = None,
126-
start_time: str = "10m",
127-
end_time: str = "now",
128-
chunk_size: str = None,
125+
start_time: datetime = (datetime.now() - timedelta(minutes=10)),
126+
end_time: datetime = datetime.now(),
127+
chunk_size: timedelta = None,
129128
store_locally: bool = False,
130129
params: dict = None,
131130
):
@@ -136,10 +135,10 @@ def get_metric_range_data(
136135
:param metric_name: (str) The name of the metric.
137136
:param label_config: (dict) A dictionary specifying metric labels and their
138137
values.
139-
:param start_time: (str) A string that specifies the metric range start time.
140-
:param end_time: (str) A string that specifies the metric range end time.
141-
:param chunk_size: (str) Duration of metric data downloaded in one request. For
142-
example, setting it to '3h' will download 3 hours worth of data in each
138+
:param start_time: (datetime) A datetime object that specifies the metric range start time.
139+
:param end_time: (datetime) A datetime object that specifies the metric range end time.
140+
:param chunk_size: (timedelta) Duration of metric data downloaded in one request. For
141+
example, setting it to timedelta(hours=3) will download 3 hours worth of data in each
143142
request made to the prometheus host
144143
:param store_locally: (bool) If set to True, will store data locally at,
145144
`"./metrics/hostname/metric_date/name_time.json.bz2"`
@@ -152,33 +151,44 @@ def get_metric_range_data(
152151
params = params or {}
153152
data = []
154153

155-
start = int(dateparser.parse(str(start_time)).timestamp())
156-
end = int(dateparser.parse(str(end_time)).timestamp())
154+
_LOGGER.debug("start_time: %s", start_time)
155+
_LOGGER.debug("end_time: %s", end_time)
156+
_LOGGER.debug("chunk_size: %s", chunk_size)
157+
158+
if not (isinstance(start_time, datetime) and isinstance(end_time, datetime)):
159+
raise TypeError("start_time and end_time can only be of type datetime.datetime")
157160

158161
if not chunk_size:
159-
chunk_seconds = int(end - start)
160-
chunk_size = str(int(chunk_seconds)) + "s"
161-
else:
162-
chunk_seconds = int(
163-
round((dateparser.parse("now") - dateparser.parse(chunk_size)).total_seconds())
164-
)
162+
chunk_size = end_time - start_time
163+
if not isinstance(chunk_size, timedelta):
164+
raise TypeError("chunk_size can only be of type datetime.timedelta")
165165

166-
if int(end - start) < chunk_seconds:
166+
start = round(start_time.timestamp())
167+
end = round(end_time.timestamp())
168+
169+
if (end_time - start_time).total_seconds() < chunk_size.total_seconds():
167170
sys.exit("specified chunk_size is too big")
171+
chunk_seconds = round(chunk_size.total_seconds())
168172

169173
if label_config:
170174
label_list = [str(key + "=" + "'" + label_config[key] + "'") for key in label_config]
171-
# print(label_list)
172175
query = metric_name + "{" + ",".join(label_list) + "}"
173176
else:
174177
query = metric_name
178+
_LOGGER.debug("Prometheus Query: %s", query)
175179

176180
while start < end:
181+
if start + chunk_seconds > end:
182+
chunk_seconds = end - start
183+
177184
# using the query API to get raw data
178185
response = requests.get(
179186
"{0}/api/v1/query".format(self.url),
180187
params={
181-
**{"query": query + "[" + chunk_size + "]", "time": start + chunk_seconds},
188+
**{
189+
"query": query + "[" + str(chunk_seconds) + "s" + "]",
190+
"time": start + chunk_seconds,
191+
},
182192
**params,
183193
},
184194
verify=self.ssl_verification,
@@ -207,7 +217,8 @@ def _store_metric_values_local(self, metric_name, values, end_timestamp, compres
207217
208218
:param metric_name: (str) the name of the metric being saved
209219
:param values: (str) metric data in JSON string format
210-
:param end_timestamp: (str) timestamp in any format understood by dateparser
220+
:param end_timestamp: (int) timestamp in any format understood by \
221+
datetime.datetime.fromtimestamp()
211222
:param compressed: (bool) whether or not to apply bz2 compression
212223
:returns: (str) path to the saved metric file
213224
"""
@@ -229,15 +240,16 @@ def _store_metric_values_local(self, metric_name, values, end_timestamp, compres
229240

230241
return file_path
231242

232-
def _metric_filename(self, metric_name: str, end_timestamp: str):
243+
def _metric_filename(self, metric_name: str, end_timestamp: int):
233244
"""
234245
Adds a timestamp to the filename before it is stored
235246
236247
:param metric_name: (str) the name of the metric being saved
237-
:param end_timestamp: (str) timestamp in any format understood by dateparser
248+
:param end_timestamp: (int) timestamp in any format understood by \
249+
datetime.datetime.fromtimestamp()
238250
:returns: (str) the generated path
239251
"""
240-
end_timestamp = dateparser.parse(str(end_timestamp))
252+
end_timestamp = datetime.fromtimestamp(end_timestamp)
241253
directory_name = end_timestamp.strftime("%Y%m%d")
242254
timestamp = end_timestamp.strftime("%Y%m%d%H%M")
243255
object_path = (
@@ -286,15 +298,3 @@ def custom_query(self, query: str, params: dict = None):
286298
)
287299

288300
return data
289-
290-
291-
def pretty_print_metric(metric_data):
292-
"""
293-
A function to pretty print the metric data downloaded using class PrometheusConnect.
294-
295-
:param metric_data: (list) This is the metric data list returned from methods
296-
get_metric_range_data and get_current_metric_value
297-
"""
298-
data = metric_data
299-
for metric in data:
300-
print(json.dumps(metric, indent=4, sort_keys=True))

prometheus_api_client/utils.py

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Some helpful functions used in the API
33
"""
4+
import json
45
import dateparser
56

67

@@ -19,3 +20,15 @@ def parse_timedelta(time_a: str = "now", time_b: str = "1d"):
1920
returns timedelta for time_a - time_b
2021
"""
2122
return parse_datetime(time_a) - parse_datetime(time_b)
23+
24+
25+
def pretty_print_metric(metric_data):
26+
"""
27+
A function to pretty print the metric data downloaded using class PrometheusConnect.
28+
29+
:param metric_data: (list) This is the metric data list returned from methods
30+
get_metric_range_data and get_current_metric_value
31+
"""
32+
data = metric_data
33+
for metric in data:
34+
print(json.dumps(metric, indent=4, sort_keys=True))

tests/test_prometheus_connect.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Test module for class PrometheusConnect
3+
"""
4+
import unittest
5+
import os
6+
from datetime import datetime, timedelta
7+
from prometheus_api_client import MetricsList, PrometheusConnect
8+
9+
10+
class TestPrometheusConnect(unittest.TestCase):
11+
"""
12+
Test module for class PrometheusConnect
13+
"""
14+
15+
def setUp(self):
16+
"""
17+
set up connection settings for prometheus
18+
"""
19+
self.prometheus_host = os.getenv("PROM_URL")
20+
self.pc = PrometheusConnect(url=self.prometheus_host, disable_ssl=True)
21+
22+
def test_metrics_list(self):
23+
"""
24+
Check if setup was done correctly
25+
"""
26+
metrics_list = self.pc.all_metrics()
27+
self.assertTrue(len(metrics_list) > 0, "no metrics received from prometheus")
28+
29+
def test_get_metric_range_data(self):
30+
start_time = datetime.now() - timedelta(minutes=10)
31+
end_time = datetime.now()
32+
metric_data = self.pc.get_metric_range_data(
33+
metric_name="up", start_time=start_time, end_time=end_time
34+
)
35+
36+
metric_objects_list = MetricsList(metric_data)
37+
38+
self.assertTrue(len(metric_objects_list) > 0, "no metrics received from prometheus")
39+
self.assertTrue(
40+
start_time.timestamp() < metric_objects_list[0].start_time.timestamp(),
41+
"invalid metric start time",
42+
)
43+
self.assertTrue(
44+
(start_time + timedelta(minutes=1)).timestamp()
45+
> metric_objects_list[0].start_time.timestamp(),
46+
"invalid metric start time",
47+
)
48+
self.assertTrue(
49+
end_time.timestamp() > metric_objects_list[0].end_time.timestamp(),
50+
"invalid metric end time",
51+
)
52+
self.assertTrue(
53+
(end_time - timedelta(minutes=1)).timestamp()
54+
< metric_objects_list[0].end_time.timestamp(),
55+
"invalid metric end time",
56+
)
57+
58+
def test_get_metric_range_data_with_chunk_size(self):
59+
start_time = datetime.now() - timedelta(minutes=65)
60+
chunk_size = timedelta(minutes=7)
61+
end_time = datetime.now() - timedelta(minutes=5)
62+
metric_data = self.pc.get_metric_range_data(
63+
metric_name="up", start_time=start_time, end_time=end_time, chunk_size=chunk_size
64+
)
65+
66+
metric_objects_list = MetricsList(metric_data)
67+
68+
self.assertTrue(len(metric_objects_list) > 0, "no metrics received from prometheus")
69+
self.assertTrue(
70+
start_time.timestamp() < metric_objects_list[0].start_time.timestamp(),
71+
"invalid metric start time (with given chunk_size)",
72+
)
73+
self.assertTrue(
74+
(start_time + timedelta(minutes=1)).timestamp()
75+
> metric_objects_list[0].start_time.timestamp(),
76+
"invalid metric start time (with given chunk_size)",
77+
)
78+
self.assertTrue(
79+
end_time.timestamp() > metric_objects_list[0].end_time.timestamp(),
80+
"invalid metric end time (with given chunk_size)",
81+
)
82+
self.assertTrue(
83+
(end_time - timedelta(minutes=1)).timestamp()
84+
< metric_objects_list[0].end_time.timestamp(),
85+
"invalid metric end time (with given chunk_size)",
86+
)
87+
88+
def test_get_metric_range_data_with_incorrect_input_types(self):
89+
start_time = datetime.now() - timedelta(minutes=20)
90+
chunk_size = timedelta(minutes=7)
91+
end_time = datetime.now() - timedelta(minutes=10)
92+
93+
with self.assertRaises(TypeError, msg="start_time accepted invalid value type"):
94+
_ = self.pc.get_metric_range_data(
95+
metric_name="up", start_time="20m", end_time=end_time, chunk_size=chunk_size
96+
)
97+
with self.assertRaises(TypeError, msg="end_time accepted invalid value type"):
98+
_ = self.pc.get_metric_range_data(
99+
metric_name="up", start_time=start_time, end_time="10m", chunk_size=chunk_size
100+
)
101+
with self.assertRaises(TypeError, msg="chunk_size accepted invalid value type"):
102+
_ = self.pc.get_metric_range_data(
103+
metric_name="up", start_time=start_time, end_time=end_time, chunk_size="10m"
104+
)

0 commit comments

Comments
 (0)