From 7e6e6972fb92b35f696975f34bcf867ef2bfbfb2 Mon Sep 17 00:00:00 2001 From: Ryan Brandt Date: Tue, 12 Jan 2016 10:01:25 -0700 Subject: [PATCH] Enhance dimension filtering Allow filtering for existence of dimension value, ex. all metrics that have a service dimension, regardless of value Change-Id: I3ffcae317e4d0bd03e8a02154858eea4afd25425 --- docs/monasca-api-spec.md | 6 +- .../app/validation/DimensionValidation.java | 6 +- .../api/app/validation/Validation.java | 4 +- .../persistence/DimensionQueries.java | 15 +++- .../SubAlarmDefinitionQueries.java | 19 ++-- .../persistence/influxdb/InfluxV9Utils.java | 15 +++- .../persistence/mysql/AlarmMySqlRepoImpl.java | 24 +++-- .../influxdb/metrics_repository.py | 22 +++-- .../repositories/mysql/alarms_repository.py | 20 ++++- monasca_api/tests/test_query_helpers.py | 16 +++- monasca_api/tests/test_validation.py | 3 +- monasca_api/v2/common/validation.py | 2 +- monasca_api/v2/reference/alarms.py | 15 ++++ monasca_api/v2/reference/helpers.py | 2 + .../tests/api/test_alarms.py | 90 +++++++++++++++++++ .../tests/api/test_metrics.py | 70 ++++++++++++++- 16 files changed, 291 insertions(+), 38 deletions(-) diff --git a/docs/monasca-api-spec.md b/docs/monasca-api-spec.md index da910fefa..8a7999c3b 100644 --- a/docs/monasca-api-spec.md +++ b/docs/monasca-api-spec.md @@ -992,7 +992,7 @@ None. #### Query Parameters * tenant_id (string, optional, restricted) - Tenant ID to from which to get metrics. This parameter can be used to get metrics from a tenant other than the tenant the request auth token is scoped to. Usage of this query parameter is restricted to users with the the monasca admin role, as defined in the monasca api configuration file, which defaults to `monasca-admin`. * name (string(255), optional) - A metric name to filter metrics by. -* dimensions (string, optional) - A dictionary to filter metrics by specified as a comma separated array of (key, value) pairs as `key1:value1,key2:value2, ...` +* dimensions (string, optional) - A dictionary to filter metrics by specified as a comma separated array of (key, value) pairs as `key1:value1,key2:value2, ...`, leaving the value empty `key1,key2:value2` will return all values for that key, multiple values for a key may be specified as `key1:value1|value2|...,key2:value4,...` * start_time (string, optional) - The start time in ISO 8601 combined date and time format in UTC. This is useful for only listing metrics that have measurements since the specified start_time. * end_time (string, optional) - The end time in ISO 8601 combined date and time format in UTC. Combined with start_time, this can be useful to only list metrics that have measurements in between the specified start_time and end_time. * offset (integer (InfluxDB) or hexadecimal string (Vertica), optional) @@ -1785,7 +1785,7 @@ None. #### Query Parameters * name (string(255), optional) - Name of alarm to filter by. -* dimensions (string, optional) - Dimensions of metrics to filter by specified as a comma separated array of (key, value) pairs as `key1:value1,key1:value1, ...` +* dimensions (string, optional) - Dimensions of metrics to filter by specified as a comma separated array of (key, value) pairs as `key1:value1,key1:value1, ...`, leaving the value empty `key1,key2:value2` will return all values for that key, multiple values for a key may be specified as `key1:value1|value2|...,key2:value4,...` * offset (integer, optional) * limit (integer, optional) * sort_by (string, optional) - Comma separated list of fields to sort by, defaults to 'id', 'created_at'. Fields may be followed by 'asc' or 'desc' to set the direction, ex 'severity desc' @@ -2251,7 +2251,7 @@ None. * alarm_definition_id (string, optional) - Alarm definition ID to filter by. * metric_name (string(255), optional) - Name of metric to filter by. -* metric_dimensions ({string(255): string(255)}, optional) - Dimensions of metrics to filter by specified as a comma separated array of (key, value) pairs as `key1:value1,key1:value1, ...` +* metric_dimensions ({string(255): string(255)}, optional) - Dimensions of metrics to filter by specified as a comma separated array of (key, value) pairs as `key1:value1,key1:value1, ...`, leaving the value empty `key1,key2:value2` will return all values for that key, multiple values for a key may be specified as `key1:value1|value2|...,key2:value4,...` * state (string, optional) - State of alarm to filter by, either `OK`, `ALARM` or `UNDETERMINED`. * lifecycle_state (string(50), optional) - Lifecycle state to filter by. * link (string(512), optional) - Link to filter by. diff --git a/java/src/main/java/monasca/api/app/validation/DimensionValidation.java b/java/src/main/java/monasca/api/app/validation/DimensionValidation.java index cc05716cf..60439baa2 100644 --- a/java/src/main/java/monasca/api/app/validation/DimensionValidation.java +++ b/java/src/main/java/monasca/api/app/validation/DimensionValidation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -33,8 +33,8 @@ public final class DimensionValidation { private static final Map VALIDATORS; private static final Pattern UUID_PATTERN = Pattern .compile("\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}"); - private static final Pattern VALID_DIMENSION_NAME = Pattern.compile("[^><={}(),\"\\\\;&]+$"); - private static final String INVALID_CHAR_STRING = "> < = { } ( ) \" \\ , ; &"; + private static final Pattern VALID_DIMENSION_NAME = Pattern.compile("[^><={}(),\"\\\\;&\\|]+$"); + private static final String INVALID_CHAR_STRING = "> < = { } ( ) \" \\ , ; & |"; private DimensionValidation() {} diff --git a/java/src/main/java/monasca/api/app/validation/Validation.java b/java/src/main/java/monasca/api/app/validation/Validation.java index 355d78625..a7fc7fae8 100644 --- a/java/src/main/java/monasca/api/app/validation/Validation.java +++ b/java/src/main/java/monasca/api/app/validation/Validation.java @@ -95,9 +95,11 @@ public final class Validation { String[] dimensionArr = Iterables.toArray(COLON_SPLITTER.split(dimensionStr), String.class); if (dimensionArr.length == 2) dimensions.put(dimensionArr[0], dimensionArr[1]); + if (dimensionArr.length == 1) + dimensions.put(dimensionArr[0], ""); } - DimensionValidation.validate(dimensions); + //DimensionValidation.validate(dimensions); return dimensions; } diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/DimensionQueries.java b/java/src/main/java/monasca/api/infrastructure/persistence/DimensionQueries.java index 2d5c14b3e..81f4b64c1 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/DimensionQueries.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/DimensionQueries.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -13,9 +13,13 @@ */ package monasca.api.infrastructure.persistence; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import org.skife.jdbi.v2.Query; @@ -38,7 +42,14 @@ public final class DimensionQueries { for (Iterator> it = dimensions.entrySet().iterator(); it.hasNext(); i++) { Map.Entry entry = it.next(); query.bind("dname" + i, entry.getKey()); - query.bind("dvalue" + i, entry.getValue()); + if (!Strings.isNullOrEmpty(entry.getValue())) { + List values = Splitter.on('|').splitToList(entry.getValue()); + int j = 0; + for (String value : values) { + query.bind("dvalue" + i + '_' + j, value); + j++; + } + } } } } diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/SubAlarmDefinitionQueries.java b/java/src/main/java/monasca/api/infrastructure/persistence/SubAlarmDefinitionQueries.java index ff1682e74..47f893d96 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/SubAlarmDefinitionQueries.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/SubAlarmDefinitionQueries.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -13,6 +13,8 @@ */ package monasca.api.infrastructure.persistence; +import com.google.common.base.Strings; + import java.util.Map; /** @@ -29,11 +31,18 @@ public final class SubAlarmDefinitionQueries { sbJoin = new StringBuilder(); - for (int i = 0; i < dimensions.size(); i++) { - sbJoin.append(" inner join sub_alarm_definition_dimension d").append(i).append(" on d").append(i) - .append(".dimension_name = :dname").append(i).append(" and d").append(i) - .append(".value = :dvalue").append(i).append(" and dim.sub_alarm_definition_id = d") + int i = 0; + for (String dimension_key : dimensions.keySet()) { + sbJoin.append(" inner join sub_alarm_definition_dimension d").append(i).append(" on d") + .append(i) + .append(".dimension_name = :dname").append(i); + if (!Strings.isNullOrEmpty(dimensions.get(dimension_key))) { + sbJoin.append(" and d").append(i) + .append(".value = :dvalue").append(i); + } + sbJoin.append(" and dim.sub_alarm_definition_id = d") .append(i).append(".sub_alarm_definition_id"); + i++; } } diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/influxdb/InfluxV9Utils.java b/java/src/main/java/monasca/api/infrastructure/persistence/influxdb/InfluxV9Utils.java index aeb710f65..3d08decad 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/influxdb/InfluxV9Utils.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/influxdb/InfluxV9Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2015,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -13,6 +13,8 @@ */ package monasca.api.infrastructure.persistence.influxdb; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; @@ -166,8 +168,15 @@ public class InfluxV9Utils { if (dims != null && !dims.isEmpty()) { for (String k : dims.keySet()) { String v = dims.get(k); - if (k != null && !k.isEmpty() && v != null && !v.isEmpty()) { - sb.append(" and \"" + sanitize(k) + "\"=" + "'" + sanitize(v) + "'"); + if (k != null && !k.isEmpty()) { + sb.append(" and \"" + sanitize(k) + "\""); + if (Strings.isNullOrEmpty(v)) { + sb.append("=~ /.*/"); + } else if (v.contains("|")) { + sb.append("=~ " + "/^" + sanitize(v) + "$/"); + } else { + sb.append("= " + "'" + sanitize(v) + "'"); + } } } } diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java index 64b1fb908..d22b7a9d3 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java @@ -14,6 +14,7 @@ package monasca.api.infrastructure.persistence.mysql; import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import monasca.api.domain.exception.EntityNotFoundException; @@ -95,15 +96,28 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { if (dimensions == null) { return; } - - for (int i = 0; i < dimensions.size(); i++) { + int i = 0; + for (String dimension_key : dimensions.keySet()) { final String indexStr = String.valueOf(i); sbJoin.append(" inner join metric_dimension md").append(indexStr).append(" on md") .append(indexStr) - .append(".name = :dname").append(indexStr).append(" and md").append(indexStr) - .append(".value = :dvalue").append(indexStr) - .append(" and mdd.metric_dimension_set_id = md") + .append(".name = :dname").append(indexStr); + String dim_value = dimensions.get(dimension_key); + if (!Strings.isNullOrEmpty(dim_value)) { + sbJoin.append(" and ("); + List values = Splitter.on('|').splitToList(dim_value); + for (int j = 0; j < values.size(); j++) { + sbJoin.append(" md").append(indexStr) + .append(".value = :dvalue").append(indexStr).append('_').append(j); + if (j < values.size() - 1) { + sbJoin.append(" or"); + } + } + sbJoin.append(")"); + } + sbJoin.append(" and mdd.metric_dimension_set_id = md") .append(indexStr).append(".dimension_set_id"); + i++; } } diff --git a/monasca_api/common/repositories/influxdb/metrics_repository.py b/monasca_api/common/repositories/influxdb/metrics_repository.py index 9dbadedd1..f1df6eba5 100644 --- a/monasca_api/common/repositories/influxdb/metrics_repository.py +++ b/monasca_api/common/repositories/influxdb/metrics_repository.py @@ -1,6 +1,6 @@ # -*- coding: utf8 -*- # Copyright 2014 Hewlett-Packard -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP # Copyright 2015 Cray Inc. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -135,11 +135,23 @@ class MetricsRepository(metrics_repository.MetricsRepository): sorted(dimensions.iteritems())): # replace ' with \' to make query parsable clean_dimension_name = dimension_name.replace("\'", "\\'") - clean_dimension_value = dimension_value.replace("\'", "\\'") + if dimension_value == "": + where_clause += " and \"{}\" =~ /.*/ ".format( + clean_dimension_name) + elif '|' in dimension_value: + # replace ' with \' to make query parsable + clean_dimension_value = dimension_value.replace("\'", "\\'") - where_clause += " and \"{}\" = '{}'".format( - clean_dimension_name.encode('utf8'), - clean_dimension_value.encode('utf8')) + where_clause += " and \"{}\" =~ /^{}$/ ".format( + clean_dimension_name.encode('utf8'), + clean_dimension_value.encode('utf8')) + else: + # replace ' with \' to make query parsable + clean_dimension_value = dimension_value.replace("\'", "\\'") + + where_clause += " and \"{}\" = '{}' ".format( + clean_dimension_name.encode('utf8'), + clean_dimension_value.encode('utf8')) if start_timestamp is not None: where_clause += " and time > " + str(int(start_timestamp * diff --git a/monasca_api/common/repositories/mysql/alarms_repository.py b/monasca_api/common/repositories/mysql/alarms_repository.py index f3b1ebcb9..909389ab1 100644 --- a/monasca_api/common/repositories/mysql/alarms_repository.py +++ b/monasca_api/common/repositories/mysql/alarms_repository.py @@ -267,15 +267,27 @@ class AlarmsRepository(mysql_repository.MySQLRepository, i = 0 for metric_dimension in query_parms['metric_dimensions']: parsed_dimension = metric_dimension.split(':') + if len(parsed_dimension) == 1: + values = None + value_sql = "" + elif '|' in parsed_dimension[1]: + values = parsed_dimension[1].encode('utf8').split('|') + value_sql = " and (" + value_sql += " or ".join(["value = %s" for j in xrange(len(values))]) + value_sql += ') ' + else: + values = [parsed_dimension[1]] + value_sql = " and value = %s " sub_select_clause += """ inner join (select distinct dimension_set_id from metric_dimension - where name = %s and value = %s) as md{} + where name = %s {}) as md{} on md{}.dimension_set_id = mdd.metric_dimension_set_id - """.format(i, i) + """.format(value_sql, i, i) i += 1 - sub_select_parms += [parsed_dimension[0].encode('utf8'), - parsed_dimension[1].encode('utf8')] + sub_select_parms.append(parsed_dimension[0].encode('utf8')) + if len(parsed_dimension) > 1 and values: + sub_select_parms.extend(values) sub_select_clause += ")" parms += sub_select_parms diff --git a/monasca_api/tests/test_query_helpers.py b/monasca_api/tests/test_query_helpers.py index 2843fcde3..05212fdae 100644 --- a/monasca_api/tests/test_query_helpers.py +++ b/monasca_api/tests/test_query_helpers.py @@ -1,4 +1,5 @@ # Copyright 2015 Cray Inc. All Rights Reserved. +# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -77,12 +78,19 @@ class TestGetQueryDimension(unittest.TestCase): "Dimension-2": "Value-2", "Dimension-3": "Value-3"}) - def test_malformed_dimension_no_value(self): + def test_dimension_no_value(self): req = Mock() - req.query_string = ("foo=bar&dimensions=no_value") + req.query_string = ("foo=bar&dimensions=Dimension_no_value") - self.assertRaises( - HTTPUnprocessableEntityError, helpers.get_query_dimensions, req) + result = helpers.get_query_dimensions(req) + self.assertEqual(result, {"Dimension_no_value": ""}) + + def test_dimension_multi_value(self): + req = Mock() + req.query_string = ("foo=bar&dimensions=Dimension_multi_value:one|two|three") + + result = helpers.get_query_dimensions(req) + self.assertEqual(result, {"Dimension_multi_value": "one|two|three"}) def test_malformed_dimension_extra_colons(self): req = Mock() diff --git a/monasca_api/tests/test_validation.py b/monasca_api/tests/test_validation.py index f757cb385..b4b81b290 100644 --- a/monasca_api/tests/test_validation.py +++ b/monasca_api/tests/test_validation.py @@ -1,5 +1,6 @@ # Copyright 2015 Hewlett-Packard # Copyright 2015 Cray Inc. All Rights Reserved. +# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -23,7 +24,7 @@ import mock import unittest -invalid_chars = "<>={}(),\"\\;&" +invalid_chars = "<>={}(),\"\\|;&" class TestMetricNameValidation(unittest.TestCase): diff --git a/monasca_api/v2/common/validation.py b/monasca_api/v2/common/validation.py index 57226d661..e7fa83655 100644 --- a/monasca_api/v2/common/validation.py +++ b/monasca_api/v2/common/validation.py @@ -16,7 +16,7 @@ from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError import re -invalid_chars = "<>={}(),'\"\\\\;&" +invalid_chars = "<>={}(),\"\\\\|;&" restricted_chars = re.compile('[' + invalid_chars + ']') diff --git a/monasca_api/v2/reference/alarms.py b/monasca_api/v2/reference/alarms.py index 1fa874f4a..fc62b2027 100644 --- a/monasca_api/v2/reference/alarms.py +++ b/monasca_api/v2/reference/alarms.py @@ -128,6 +128,7 @@ class Alarms(alarms_api_v2.AlarmsV2API, # ensure metric_dimensions is a list if 'metric_dimensions' in query_parms and isinstance(query_parms['metric_dimensions'], str): query_parms['metric_dimensions'] = query_parms['metric_dimensions'].split(',') + self._validate_dimensions(query_parms['metric_dimensions']) offset = helpers.get_query_param(req, 'offset') if offset is not None and not isinstance(offset, int): @@ -152,6 +153,20 @@ class Alarms(alarms_api_v2.AlarmsV2API, res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 + @staticmethod + def _validate_dimensions(dimensions): + try: + assert isinstance(dimensions, list) + for dimension in dimensions: + name_value = dimension.split('=') + validation.dimension_key(name_value[0]) + if len(name_value) > 1 and '|' in name_value[1]: + values = name_value[1].split('|') + for value in values: + validation.dimension_value(value) + except Exception as e: + raise HTTPUnprocessableEntityError("Unprocessable Entity", e.message) + @resource.resource_try_catch_block def _alarm_update(self, tenant_id, alarm_id, new_state, lifecycle_state, link): diff --git a/monasca_api/v2/reference/helpers.py b/monasca_api/v2/reference/helpers.py index cca8c5acd..51de38b41 100644 --- a/monasca_api/v2/reference/helpers.py +++ b/monasca_api/v2/reference/helpers.py @@ -184,6 +184,8 @@ def get_query_dimensions(req): if len(dimension_name_value) == 2: dimensions[dimension_name_value[0]] = dimension_name_value[ 1] + elif len(dimension_name_value) == 1: + dimensions[dimension_name_value[0]] = "" else: raise Exception('Dimensions are malformed') return dimensions diff --git a/monasca_tempest_tests/tests/api/test_alarms.py b/monasca_tempest_tests/tests/api/test_alarms.py index e09b32658..a51bdaaa2 100644 --- a/monasca_tempest_tests/tests/api/test_alarms.py +++ b/monasca_tempest_tests/tests/api/test_alarms.py @@ -89,6 +89,96 @@ class TestAlarms(base.BaseMonascaTest): self.assertEqual(alarm_definition_ids[0], element['alarm_definition']['id']) + @test.attr(type="gate") + def test_list_alarms_by_metric_dimensions_no_value(self): + metric_name = data_utils.rand_name('metric') + match_by_key = data_utils.rand_name('key') + dim_key = data_utils.rand_name('key') + alarm_def = helpers.create_alarm_definition( + name=data_utils.rand_name('definition'), + expression=metric_name + " > 1", + match_by=[match_by_key]) + metric_1 = helpers.create_metric(metric_name, + {match_by_key: data_utils.rand_name('value'), + dim_key: data_utils.rand_name('value')}) + metric_2 = helpers.create_metric(metric_name, + {match_by_key: data_utils.rand_name('value'), + dim_key: data_utils.rand_name('value')}) + metric_3 = helpers.create_metric(metric_name, + {match_by_key: data_utils.rand_name('value')}) + metrics = [metric_1, metric_2, metric_3] + resp, response_body = self.monasca_client.create_alarm_definitions(alarm_def) + self.assertEqual(201, resp.status) + + for i in xrange(constants.MAX_RETRIES): + resp, alarm_def_result = self.monasca_client.create_metrics(metrics) + self.assertEqual(204, resp.status) + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + metric_name) + self.assertEqual(200, resp.status) + if len(response_body['elements']) >= 3: + break + time.sleep(constants.RETRY_WAIT_SECS) + if i >= constants.MAX_RETRIES - 1: + self.fail("Timeout creating alarms, required 3 but found {}".format( + len(response_body['elements']))) + + query_parms = '?metric_dimensions=' + dim_key + resp, response_body = self.monasca_client.list_alarms(query_parms) + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=2) + dimension_sets = [] + for element in response_body['elements']: + self.assertEqual(metric_name, element['metrics'][0]['name']) + dimension_sets.append(element['metrics'][0]['dimensions']) + self.assertIn(metric_1['dimensions'], dimension_sets) + self.assertIn(metric_2['dimensions'], dimension_sets) + self.assertNotIn(metric_3['dimensions'], dimension_sets) + + + @test.attr(type="gate") + def test_list_alarms_by_metric_dimensions_multi_value(self): + metric_name = data_utils.rand_name('metric') + match_by_key = data_utils.rand_name('key') + dim_key = data_utils.rand_name('key') + dim_value_1 = data_utils.rand_name('value') + dim_value_2 = data_utils.rand_name('value') + alarm_def = helpers.create_alarm_definition( + name=data_utils.rand_name('definition'), + expression=metric_name + " > 1", + match_by=[match_by_key]) + metric_1 = helpers.create_metric(metric_name, {match_by_key: data_utils.rand_name('value'), + dim_key: dim_value_1}) + metric_2 = helpers.create_metric(metric_name, {match_by_key: data_utils.rand_name('value'), + dim_key: dim_value_2}) + metric_3 = helpers.create_metric(metric_name, {match_by_key: data_utils.rand_name('value')}) + metrics = [metric_1, metric_2, metric_3] + resp, response_body = self.monasca_client.create_alarm_definitions(alarm_def) + self.assertEqual(201, resp.status) + for i in xrange(constants.MAX_RETRIES): + resp, alarm_def_result = self.monasca_client.create_metrics(metrics) + self.assertEqual(204, resp.status) + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + metric_name) + self.assertEqual(200, resp.status) + if len(response_body['elements']) >= 3: + return + time.sleep(constants.RETRY_WAIT_SECS) + if i >= constants.MAX_RETRIES - 1: + self.fail("Timeout creating alarms, required 3 but found {}".format( + len(response_body['elements']))) + + query_parms = '?metric_dimensions=' + dim_key + ':' + dim_value_1 + '|' + dim_value_2 + resp, response_body = self.monasca_client.list_alarms(query_parms) + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=2) + dimension_sets = [] + for element in response_body['elements']: + self.assertEqual(metric_name, element['metrics'][0]['name']) + dimension_sets.append(element['metrics'][0]['dimensions']) + self.assertIn(metric_1['dimensions'], dimension_sets) + self.assertIn(metric_2['dimensions'], dimension_sets) + self.assertNotIn(metric_3['dimensions'], dimension_sets) + + @test.attr(type="gate") def test_list_alarms_by_state(self): helpers.delete_alarm_definitions(self.monasca_client) diff --git a/monasca_tempest_tests/tests/api/test_metrics.py b/monasca_tempest_tests/tests/api/test_metrics.py index 19f31999f..9de8a3ddf 100644 --- a/monasca_tempest_tests/tests/api/test_metrics.py +++ b/monasca_tempest_tests/tests/api/test_metrics.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP # # 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 @@ -290,6 +290,74 @@ class TestMetrics(base.BaseMonascaTest): "metrics = 0" self.fail(error_msg) + @test.attr(type='gate') + def test_list_metrics_dimension_query_multi_value(self): + name = data_utils.rand_name('name') + key_service = "service" + value_1 = data_utils.rand_name('value') + value_2 = data_utils.rand_name('value') + metric_1 = helpers.create_metric(name, {key_service: value_1}) + metric_2 = helpers.create_metric(name, {key_service: value_2}) + metric_3 = helpers.create_metric(name) + metrics = [metric_1, metric_2, metric_3] + resp, response_body = self.monasca_client.create_metrics(metrics) + self.assertEqual(204, resp.status) + query_param = '?name=' + name + '&dimensions=service:' + value_1 + '|' + value_2 + for i in xrange(constants.MAX_RETRIES): + resp, response_body = self.monasca_client.list_metrics(query_param) + self.assertEqual(200, resp.status) + elements = response_body['elements'] + if len(elements) == 2: + dimension_sets = [] + for element in elements: + self.assertEqual(name, element['name']) + dimension_sets.append(element['dimensions']) + self.assertIn(metric_1['dimensions'], dimension_sets) + self.assertIn(metric_2['dimensions'], dimension_sets) + self.assertNotIn(metric_3['dimensions'], dimension_sets) + return + + time.sleep(constants.RETRY_WAIT_SECS) + if i == constants.MAX_RETRIES - 1: + error_msg = "Timeout on waiting for metrics: at least " \ + "2 metrics are needed. Current number of " \ + "metrics = 0" + self.fail(error_msg) + + @test.attr(type='gate') + def test_list_metrics_dimension_query_no_value(self): + name = data_utils.rand_name('name') + key_service = "service" + value_1 = data_utils.rand_name('value') + value_2 = data_utils.rand_name('value') + metric_1 = helpers.create_metric(name, {key_service: value_1}) + metric_2 = helpers.create_metric(name, {key_service: value_2}) + metric_3 = helpers.create_metric(name) + metrics = [metric_1, metric_2, metric_3] + resp, response_body = self.monasca_client.create_metrics(metrics) + self.assertEqual(204, resp.status) + query_param = '?name=' + name + '&dimensions=service' + for i in xrange(constants.MAX_RETRIES): + resp, response_body = self.monasca_client.list_metrics(query_param) + self.assertEqual(200, resp.status) + elements = response_body['elements'] + if len(elements) == 2: + dimension_sets = [] + for element in elements: + self.assertEqual(name, element['name']) + dimension_sets.append(element['dimensions']) + self.assertIn(metric_1['dimensions'], dimension_sets) + self.assertIn(metric_2['dimensions'], dimension_sets) + self.assertNotIn(metric_3['dimensions'], dimension_sets) + return + + time.sleep(constants.RETRY_WAIT_SECS) + if i == constants.MAX_RETRIES - 1: + error_msg = "Timeout on waiting for metrics: at least " \ + "2 metrics are needed. Current number of " \ + "metrics = 0" + self.fail(error_msg) + @test.attr(type='gate') def test_list_metrics_with_name(self): name = data_utils.rand_name('name')