diff --git a/docs/monasca-api-spec.md b/docs/monasca-api-spec.md index c7f922934..f4529f64a 100644 --- a/docs/monasca-api-spec.md +++ b/docs/monasca-api-spec.md @@ -361,7 +361,7 @@ Optionally, a measurement may also contain extra data about the value which is k For an example of how value meta is used, imagine this metric: http_status{url: http://localhost:8080/healthcheck, hostname=devstack, service=object-storage}. The measurements for this metric have a value of either 1 or 0 depending if the status check succeeded. If the check fails, it would be helpful to have the actual http status code and error message if possible. So instead of just a value, the measurement will be something like: {Timestamp=now(), value=1, value_meta{http_rc=500, error=“Error accessing MySQL”}} -Up to 16 separate key/value pairs of value meta are allowed per measurement. The keys are required and are trimmed of leading and trailing whitespace and have a maximum length of 255 characters. The value is a string and has a maximum length of 2048 characters. The value can be an empty string. Whitespace is not trimmed from the values. +Up to 16 separate key/value pairs of value meta are allowed per measurement. The keys are required and are trimmed of leading and trailing whitespace and have a maximum length of 255 characters. The value is a string and value meta (with key, value and '{"":""}' combined) has a maximum length of 2048 characters. The value can be an empty string. Whitespace is not trimmed from the values. ## Alarm Definitions and Alarms @@ -881,7 +881,7 @@ Consists of a single metric object or an array of metric objects. A metric has t * dimensions ({string(255): string(255)}, optional) - A dictionary consisting of (key, value) pairs used to uniquely identify a metric. * timestamp (string, required) - The timestamp in milliseconds from the Epoch. * value (float, required) - Value of the metric. Values with base-10 exponents greater than 126 or less than -130 are truncated. -* value_meta ({string(255): string(2048)}, optional) - A dictionary consisting of (key, value) pairs used to add information about the value. +* value_meta ({string(255): string}(2048), optional) - A dictionary consisting of (key, value) pairs used to add information about the value. Value_meta key value combinations must be 2048 characters or less including '{"":""}' 7 characters total from every json string. The name and dimensions are used to uniquely identify a metric. diff --git a/monasca_api/tests/test_validation.py b/monasca_api/tests/test_validation.py index ae541a3eb..8182538e7 100644 --- a/monasca_api/tests/test_validation.py +++ b/monasca_api/tests/test_validation.py @@ -1,6 +1,5 @@ -# Copyright 2015 Hewlett-Packard +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP # 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 @@ -99,6 +98,44 @@ class TestDimensionValidation(unittest.TestCase): self.assertRaises(AssertionError, validation.dimension_value, dim_value) +class TestValueMetaValidation(unittest.TestCase): + def test_valid_name(self): + value_meta_name = "this.is_a.valid-name" + value_meta = {value_meta_name: 'value_meta_value'} + validation.validate_value_meta(value_meta) + self.assertTrue(True) + + def test_nonstring_name(self): + value_meta_name = 123456 + value_meta = {value_meta_name: 'value_meta_value'} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + def test_long_name(self): + value_meta_name = "x" * 256 + value_meta = {value_meta_name: 'value_meta_value'} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + def test_valid_value(self): + value_meta_value = "this.is_a.valid-value" + value_meta = {'value_meta_name': value_meta_value} + validation.validate_value_meta(value_meta) + self.assertTrue(True) + + def test_nonstring_value(self): + value_meta_value = 123456 + value_meta = {'value_meta_name': value_meta_value} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + def test_long_value_meta(self): + value_meta_value = "x" * 2048 + value_meta = {'value_meta_name': value_meta_value} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + class TestRoleValidation(unittest.TestCase): def test_role_valid(self): diff --git a/monasca_api/v2/common/validation.py b/monasca_api/v2/common/validation.py index cea8c4b3f..a04528672 100644 --- a/monasca_api/v2/common/validation.py +++ b/monasca_api/v2/common/validation.py @@ -14,6 +14,7 @@ from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError +import json import re invalid_chars = "<>={}(),\"\\\\|;&" @@ -23,6 +24,12 @@ VALID_ALARM_STATES = ["ALARM", "OK", "UNDETERMINED"] VALID_ALARM_DEFINITION_SEVERITIES = ["LOW", "MEDIUM", "HIGH", "CRITICAL"] +VALUE_META_MAX_NUMBER = 16 + +VALUE_META_MAX_LENGTH = 2048 + +VALUE_META_NAME_MAX_LENGTH = 255 + def metric_name(name): assert isinstance(name, (str, unicode)), "Metric name must be a string" @@ -75,3 +82,23 @@ def validate_sort_by(sort_by_list, allowed_sort_by): raise HTTPUnprocessableEntityError("Unprocessable Entity", "sort_by value {} must be 'asc' or 'desc'".format( sort_by_values[1])) + + +def validate_value_meta(value_meta): + value_meta_string = json.dumps(value_meta) + # entries + assert len(value_meta) <= VALUE_META_MAX_NUMBER, "ValueMeta entries must be {} or less".format( + VALUE_META_MAX_NUMBER) + # total length + assert len(value_meta_string) <= VALUE_META_MAX_LENGTH, \ + "ValueMeta name value combinations must be {} characters or less".format( + VALUE_META_MAX_LENGTH) + for name in value_meta: + # name + assert isinstance(name, (str, unicode)), "ValueMeta name must be a string" + assert len(name) <= VALUE_META_NAME_MAX_LENGTH, "ValueMeta name must be {} characters or less".format( + VALUE_META_NAME_MAX_LENGTH) + assert len(name) >= 1, "ValueMeta name cannot be empty" + # value + assert isinstance(value_meta[name], (str, unicode)), "ValueMeta value must be a string" + assert len(value_meta[name]) >= 1, "ValueMeta value cannot be empty" diff --git a/monasca_api/v2/reference/metrics.py b/monasca_api/v2/reference/metrics.py index 446355576..c4d788bb2 100644 --- a/monasca_api/v2/reference/metrics.py +++ b/monasca_api/v2/reference/metrics.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# (C) Copyright 2014, 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 @@ -78,7 +78,7 @@ class Metrics(metrics_api_v2.MetricsV2API): else: self._validate_single_metric(metrics) except Exception as ex: - LOG.debug(ex) + LOG.exception(ex) raise HTTPUnprocessableEntityError('Unprocessable Entity', ex.message) def _validate_single_metric(self, metric): @@ -89,9 +89,10 @@ class Metrics(metrics_api_v2.MetricsV2API): for dimension_key in metric['dimensions']: validation.dimension_key(dimension_key) validation.dimension_value(metric['dimensions'][dimension_key]) + if "value_meta" in metric: + validation.validate_value_meta(metric['value_meta']) def _send_metrics(self, metrics): - try: self._message_queue.send_message_batch(metrics) except message_queue_exceptions.MessageQueueException as ex: diff --git a/monasca_tempest_tests/tests/api/constants.py b/monasca_tempest_tests/tests/api/constants.py index baa582ec1..1ccc504b3 100644 --- a/monasca_tempest_tests/tests/api/constants.py +++ b/monasca_tempest_tests/tests/api/constants.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 @@ -39,3 +39,6 @@ MAX_ALARM_METRIC_NAME_LENGTH = 255 MAX_ALARM_METRIC_DIMENSIONS_KEY_LENGTH = 255 MAX_ALARM_METRIC_DIMENSIONS_VALUE_LENGTH = 255 MAX_ALARM_LINK_LENGTH = 512 + +MAX_VALUE_META_NAME_LENGTH = 255 +MAX_VALUE_META_TOTAL_LENGTH = 2048 diff --git a/monasca_tempest_tests/tests/api/test_metrics.py b/monasca_tempest_tests/tests/api/test_metrics.py index a4c6abcef..455548075 100644 --- a/monasca_tempest_tests/tests/api/test_metrics.py +++ b/monasca_tempest_tests/tests/api/test_metrics.py @@ -282,6 +282,32 @@ class TestMetrics(base.BaseMonascaTest): self.monasca_client.create_metrics, metric) + @test.attr(type='gate') + @test.attr(type=['negative']) + def test_create_metric_with_value_meta_name_exceeds_max_length(self): + long_value_meta_name = "x" * (constants.MAX_VALUE_META_NAME_LENGTH + 1) + metric = helpers.create_metric(name='name', + value_meta= + {long_value_meta_name: + "value_meta_value"} + ) + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.create_metrics, + metric) + + @test.attr(type='gate') + @test.attr(type=['negative']) + def test_create_metric_with_value_meta_exceeds_max_length(self): + value_meta_name = "x" + long_value_meta_value = "y" * constants.MAX_VALUE_META_TOTAL_LENGTH + metric = helpers.create_metric(name='name', + value_meta= + {value_meta_name: long_value_meta_value} + ) + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.create_metrics, + metric) + @test.attr(type='gate') def test_list_metrics(self): resp, response_body = self.monasca_client.list_metrics()