Skip to content

Commit 591106a

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Allow Multiple Ratings for the same Metric on Prometheus"
2 parents 37ae969 + afa6f98 commit 591106a

File tree

5 files changed

+230
-67
lines changed

5 files changed

+230
-67
lines changed

cloudkitty/collector/__init__.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,16 @@
6464
def MetricDict(value):
6565
if isinstance(value, dict) and len(value.keys()) > 0:
6666
return value
67-
raise Invalid("Not a dict with at least one key")
67+
if isinstance(value, list) and len(value) > 0:
68+
for v in value:
69+
if not (isinstance(v, dict) and len(v.keys()) > 0):
70+
raise Invalid("Not a dict with at least one key or a "
71+
"list with at least one dict with at "
72+
"least one key. Provided value: %s" % value)
73+
return value
74+
raise Invalid("Not a dict with at least one key or a "
75+
"list with at least one dict with at "
76+
"least one key. Provided value: %s" % value)
6877

6978

7079
CONF_BASE_SCHEMA = {Required('metrics'): MetricDict}
@@ -189,9 +198,26 @@ def check_configuration(conf):
189198

190199
output = {}
191200
for metric_name, metric in conf['metrics'].items():
192-
output[metric_name] = metric_schema(metric)
193-
if scope_key not in output[metric_name]['groupby']:
194-
output[metric_name]['groupby'].append(scope_key)
201+
if not isinstance(metric, list):
202+
metric = [metric]
203+
for m in metric:
204+
met = metric_schema(m)
205+
names = [metric_name]
206+
alt_name = met.get('alt_name')
207+
if alt_name is not None:
208+
names.append(alt_name)
209+
210+
new_metric_name = "@#".join(names)
211+
if output.get(new_metric_name) is not None:
212+
raise InvalidConfiguration(
213+
"Metric {} already exists, you should change the"
214+
"alt_name for metric: {}"
215+
.format(new_metric_name, metric))
216+
217+
output[new_metric_name] = met
218+
219+
if scope_key not in output[new_metric_name]['groupby']:
220+
output[new_metric_name]['groupby'].append(scope_key)
195221

196222
return output
197223

cloudkitty/collector/gnocchi.py

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from oslo_log import log as logging
2626
from voluptuous import All
2727
from voluptuous import In
28-
from voluptuous import Invalid
2928
from voluptuous import Length
3029
from voluptuous import Range
3130
from voluptuous import Required
@@ -40,24 +39,6 @@
4039

4140
COLLECTOR_GNOCCHI_OPTS = 'collector_gnocchi'
4241

43-
44-
def GnocchiMetricDict(value):
45-
if isinstance(value, dict) and len(value.keys()) > 0:
46-
return value
47-
if isinstance(value, list) and len(value) > 0:
48-
for v in value:
49-
if not (isinstance(v, dict) and len(v.keys()) > 0):
50-
raise Invalid("Not a dict with at least one key or a "
51-
"list with at least one dict with at "
52-
"least one key. Provided value: %s" % value)
53-
return value
54-
raise Invalid("Not a dict with at least one key or a "
55-
"list with at least one dict with at "
56-
"least one key. Provided value: %s" % value)
57-
58-
59-
GNOCCHI_CONF_SCHEMA = {Required('metrics'): GnocchiMetricDict}
60-
6142
collector_gnocchi_opts = [
6243
cfg.StrOpt(
6344
'gnocchi_auth_type',
@@ -202,30 +183,18 @@ def check_configuration(conf):
202183
"""Check metrics configuration
203184
204185
"""
205-
conf = Schema(GNOCCHI_CONF_SCHEMA)(conf)
206-
conf = copy.deepcopy(conf)
207-
scope_key = CONF.collect.scope_key
186+
conf = collector.BaseCollector.check_configuration(conf)
187+
208188
metric_schema = Schema(collector.METRIC_BASE_SCHEMA).extend(
209189
GNOCCHI_EXTRA_SCHEMA)
210190

211191
output = {}
212-
for metric_name, metric in conf['metrics'].items():
213-
if not isinstance(metric, list):
214-
metric = [metric]
215-
for m in metric:
216-
met = metric_schema(m)
217-
m.update(met)
218-
if scope_key not in m['groupby']:
219-
m['groupby'].append(scope_key)
220-
if met['extra_args']['resource_key'] not in m['groupby']:
221-
m['groupby'].append(met['extra_args']['resource_key'])
222-
223-
names = [metric_name]
224-
alt_name = met.get('alt_name')
225-
if alt_name is not None:
226-
names.append(alt_name)
227-
new_metric_name = "@#".join(names)
228-
output[new_metric_name] = m
192+
for metric_name, metric in conf.items():
193+
met = metric_schema(metric)
194+
if met['extra_args']['resource_key'] not in met['groupby']:
195+
met['groupby'].append(met['extra_args']['resource_key'])
196+
197+
output[metric_name] = met
229198

230199
return output
231200

cloudkitty/collector/prometheus.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -153,24 +153,24 @@ def _format_data(self, metric_name, scope_key, scope_id, start, end, data):
153153

154154
return metadata, groupby, qty
155155

156-
def fetch_all(self, metric_name, start, end, scope_id, q_filter=None):
157-
"""Returns metrics to be valorized."""
158-
scope_key = CONF.collect.scope_key
159-
method = self.conf[metric_name]['extra_args']['aggregation_method']
160-
query_function = self.conf[metric_name]['extra_args'].get(
156+
@staticmethod
157+
def build_query(conf, metric_name, start, end, scope_key, scope_id,
158+
groupby, metadata):
159+
"""Builds the query for the metrics to be valorized."""
160+
161+
method = conf[metric_name]['extra_args']['aggregation_method']
162+
query_function = conf[metric_name]['extra_args'].get(
161163
'query_function')
162-
range_function = self.conf[metric_name]['extra_args'].get(
164+
range_function = conf[metric_name]['extra_args'].get(
163165
'range_function')
164-
groupby = self.conf[metric_name].get('groupby', [])
165-
metadata = self.conf[metric_name].get('metadata', [])
166-
query_prefix = self.conf[metric_name]['extra_args']['query_prefix']
167-
query_suffix = self.conf[metric_name]['extra_args']['query_suffix']
166+
query_prefix = conf[metric_name]['extra_args']['query_prefix']
167+
query_suffix = conf[metric_name]['extra_args']['query_suffix']
168+
query_metric = metric_name.split('@#')[0]
168169
period = tzutils.diff_seconds(end, start)
169-
time = end
170170

171171
# The metric with the period
172172
query = '{0}{{{1}="{2}"}}[{3}s]'.format(
173-
metric_name,
173+
query_metric,
174174
scope_key,
175175
scope_id,
176176
period
@@ -212,6 +212,26 @@ def fetch_all(self, metric_name, start, end, scope_id, q_filter=None):
212212
if query_suffix:
213213
query = "{0} {1}".format(query, query_suffix)
214214

215+
return query
216+
217+
def fetch_all(self, metric_name, start, end, scope_id, q_filter=None):
218+
"""Returns metrics to be valorized."""
219+
time = end
220+
metadata = self.conf[metric_name].get('metadata', [])
221+
groupby = self.conf[metric_name].get('groupby', [])
222+
scope_key = CONF.collect.scope_key
223+
224+
query = self.build_query(
225+
self.conf,
226+
metric_name,
227+
start,
228+
end,
229+
scope_key,
230+
scope_id,
231+
groupby,
232+
metadata
233+
)
234+
215235
try:
216236
res = self._conn.get_instant(
217237
query,

cloudkitty/tests/collectors/test_validation.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
from cloudkitty import collector
2121
from cloudkitty import tests
22+
from datetime import datetime
23+
from datetime import timedelta
2224

2325

2426
class MetricConfigValidationTest(tests.TestCase):
@@ -44,6 +46,46 @@ class MetricConfigValidationTest(tests.TestCase):
4446
}
4547
}
4648

49+
list_data = {
50+
'metrics': {
51+
'metric_one': [
52+
{
53+
'groupby': ['one'],
54+
'metadata': ['two'],
55+
'alt_name': 'metric_u',
56+
'unit': 'u',
57+
},
58+
{
59+
'groupby': ['three'],
60+
'metadata': ['four'],
61+
'alt_name': 'metric_v',
62+
'unit': 'v',
63+
}
64+
]
65+
}
66+
}
67+
68+
list_output = {
69+
'metric_one@#metric_u': {
70+
'groupby': ['one'],
71+
'metadata': ['two'],
72+
'unit': 'u',
73+
'alt_name': 'metric_u',
74+
'factor': 1,
75+
'offset': 0,
76+
'mutate': 'NONE',
77+
},
78+
'metric_one@#metric_v': {
79+
'groupby': ['three'],
80+
'metadata': ['four'],
81+
'unit': 'v',
82+
'alt_name': 'metric_v',
83+
'factor': 1,
84+
'offset': 0,
85+
'mutate': 'NONE',
86+
},
87+
}
88+
4789
def test_base_minimal_config(self):
4890
data = copy.deepcopy(self.base_data)
4991
expected_output = copy.deepcopy(self.base_output)
@@ -149,6 +191,48 @@ def test_prometheus_minimal_config_minimal_extra_args(self):
149191
expected_output,
150192
)
151193

194+
def test_prometheus_query_builder(self):
195+
data = copy.deepcopy(self.base_data)
196+
data['metrics']['metric_one']['extra_args'] = {
197+
'aggregation_method': 'max',
198+
'query_function': 'abs',
199+
'query_prefix': 'custom_prefix',
200+
'query_suffix': 'custom_suffix',
201+
'range_function': 'delta',
202+
}
203+
204+
prometheus = collector.prometheus.PrometheusCollector
205+
206+
conf = prometheus.check_configuration(data)
207+
metric_name = list(conf.keys())[0]
208+
start = datetime.now()
209+
end = start + timedelta(seconds=60)
210+
scope_key = "random_key"
211+
scope_id = "random_value"
212+
groupby = conf[metric_name].get('groupby', [])
213+
metadata = conf[metric_name].get('metadata', [])
214+
215+
query = prometheus.build_query(
216+
conf,
217+
metric_name,
218+
start,
219+
end,
220+
scope_key,
221+
scope_id,
222+
groupby,
223+
metadata
224+
)
225+
226+
expected_output = (
227+
'custom_prefix max(abs(delta(metric_one{random_key="random_value"}'
228+
'[60s]))) by (one, project_id, two) custom_suffix'
229+
)
230+
231+
self.assertEqual(
232+
query,
233+
expected_output,
234+
)
235+
152236
def test_check_duplicates(self):
153237
data = copy.deepcopy(self.base_data)
154238
for metric_name, metric in data['metrics'].items():
@@ -179,3 +263,29 @@ def test_validate_map_mutator(self):
179263
self.assertRaises(
180264
collector.InvalidConfiguration,
181265
collector.validate_map_mutator, metric_name, metric)
266+
267+
def test_base_minimal_config_list(self):
268+
data = copy.deepcopy(self.list_data)
269+
expected_output = copy.deepcopy(self.list_output)
270+
271+
for _, metric in expected_output.items():
272+
metric['groupby'].append('project_id')
273+
274+
self.assertEqual(
275+
collector.BaseCollector.check_configuration(data),
276+
expected_output,
277+
)
278+
279+
# submetric with same alt_name should fail
280+
# Because they would overlap in the dict
281+
def test_check_duplicates_list(self):
282+
data = copy.deepcopy(self.list_data)
283+
data['metrics']['metric_one'].append({
284+
'groupby': ['five'],
285+
'metadata': ['six'],
286+
'alt_name': 'metric_v',
287+
'unit': 'w',
288+
})
289+
self.assertRaises(
290+
collector.InvalidConfiguration,
291+
collector.BaseCollector.check_configuration, data)

0 commit comments

Comments
 (0)