From cca0d9f7d78b4c3af42d71719b5713b7a8c39aff Mon Sep 17 00:00:00 2001 From: Dantali0n Date: Wed, 26 Aug 2020 16:01:15 +0200 Subject: [PATCH] Implements base method for time series metrics Implements base method as well as some basic implementations to retrieve time series metrics. Ceilometer can not be supported as API documentation has been unavailable. Grafana will be supported in follow-up patch. Partially Implements: blueprint time-series-framework Change-Id: I55414093324c8cff379b28f5b855f41a9265c2d3 --- watcher/decision_engine/datasources/base.py | 33 ++++++++++++ .../decision_engine/datasources/ceilometer.py | 10 ++-- .../decision_engine/datasources/gnocchi.py | 51 +++++++++++++++++-- .../decision_engine/datasources/grafana.py | 7 +++ .../decision_engine/datasources/monasca.py | 33 ++++++++++-- .../datasources/test_gnocchi_helper.py | 31 ++++++++++- .../datasources/test_monasca_helper.py | 39 +++++++++++++- 7 files changed, 191 insertions(+), 13 deletions(-) diff --git a/watcher/decision_engine/datasources/base.py b/watcher/decision_engine/datasources/base.py index 414ecffd0..7a58d3e64 100644 --- a/watcher/decision_engine/datasources/base.py +++ b/watcher/decision_engine/datasources/base.py @@ -19,6 +19,8 @@ import time from oslo_config import cfg from oslo_log import log +from watcher.common import exception + CONF = cfg.CONF LOG = log.getLogger(__name__) @@ -54,6 +56,13 @@ class DataSourceBase(object): instance_root_disk_size=None, ) + def _get_meter(self, meter_name): + """Retrieve the meter from the metric map or raise error""" + meter = self.METRIC_MAP.get(meter_name) + if meter is None: + raise exception.MetricNotAvailable(metric=meter_name) + return meter + def query_retry(self, f, *args, **kwargs): """Attempts to retrieve metrics from the external service @@ -122,6 +131,30 @@ class DataSourceBase(object): pass + @abc.abstractmethod + def statistic_series(self, resource=None, resource_type=None, + meter_name=None, start_time=None, end_time=None, + granularity=300): + """Retrieves metrics based on the specified parameters over a period + + :param resource: Resource object as defined in watcher models such as + ComputeNode and Instance + :param resource_type: Indicates which type of object is supplied + to the resource parameter + :param meter_name: The desired metric to retrieve as key from + METRIC_MAP + :param start_time: The datetime to start retrieving metrics for + :type start_time: datetime.datetime + :param end_time: The datetime to limit the retrieval of metrics to + :type end_time: datetime.datetime + :param granularity: Interval between samples in measurements in + seconds + :return: Dictionary of key value pairs with timestamps and metric + values + """ + + pass + @abc.abstractmethod def get_host_cpu_usage(self, resource, period, aggregate, granularity=None): diff --git a/watcher/decision_engine/datasources/ceilometer.py b/watcher/decision_engine/datasources/ceilometer.py index 316e8edd9..a20fd42c4 100644 --- a/watcher/decision_engine/datasources/ceilometer.py +++ b/watcher/decision_engine/datasources/ceilometer.py @@ -161,9 +161,7 @@ class CeilometerHelper(base.DataSourceBase): end_time = datetime.datetime.utcnow() start_time = end_time - datetime.timedelta(seconds=int(period)) - meter = self.METRIC_MAP.get(meter_name) - if meter is None: - raise exception.MetricNotAvailable(metric=meter_name) + meter = self._get_meter(meter_name) if aggregate == 'mean': aggregate = 'avg' @@ -194,6 +192,12 @@ class CeilometerHelper(base.DataSourceBase): item_value *= 10 return item_value + def statistic_series(self, resource=None, resource_type=None, + meter_name=None, start_time=None, end_time=None, + granularity=300): + raise NotImplementedError( + _('Ceilometer helper does not support statistic series method')) + def get_host_cpu_usage(self, resource, period, aggregate, granularity=None): diff --git a/watcher/decision_engine/datasources/gnocchi.py b/watcher/decision_engine/datasources/gnocchi.py index 74100238c..6a52845ca 100644 --- a/watcher/decision_engine/datasources/gnocchi.py +++ b/watcher/decision_engine/datasources/gnocchi.py @@ -23,7 +23,6 @@ from oslo_config import cfg from oslo_log import log from watcher.common import clients -from watcher.common import exception from watcher.decision_engine.datasources import base CONF = cfg.CONF @@ -72,9 +71,7 @@ class GnocchiHelper(base.DataSourceBase): stop_time = datetime.utcnow() start_time = stop_time - timedelta(seconds=(int(period))) - meter = self.METRIC_MAP.get(meter_name) - if meter is None: - raise exception.MetricNotAvailable(metric=meter_name) + meter = self._get_meter(meter_name) if aggregate == 'count': aggregate = 'mean' @@ -123,6 +120,52 @@ class GnocchiHelper(base.DataSourceBase): return return_value + def statistic_series(self, resource=None, resource_type=None, + meter_name=None, start_time=None, end_time=None, + granularity=300): + + meter = self._get_meter(meter_name) + + resource_id = resource.uuid + if resource_type == 'compute_node': + resource_id = "%s_%s" % (resource.hostname, resource.hostname) + kwargs = dict(query={"=": {"original_resource_id": resource_id}}, + limit=1) + resources = self.query_retry( + f=self.gnocchi.resource.search, **kwargs) + + if not resources: + LOG.warning("The {0} resource {1} could not be " + "found".format(self.NAME, resource_id)) + return + + resource_id = resources[0]['id'] + + raw_kwargs = dict( + metric=meter, + start=start_time, + stop=end_time, + resource_id=resource_id, + granularity=granularity, + ) + + kwargs = {k: v for k, v in raw_kwargs.items() if k and v} + + statistics = self.query_retry( + f=self.gnocchi.metric.get_measures, **kwargs) + + return_value = None + if statistics: + # measure has structure [time, granularity, value] + if meter_name == 'host_airflow': + # Airflow from hardware.ipmi.node.airflow is reported as + # 1/10 th of actual CFM + return_value = {s[0]: s[2]*10 for s in statistics} + else: + return_value = {s[0]: s[2] for s in statistics} + + return return_value + def get_host_cpu_usage(self, resource, period, aggregate, granularity=300): diff --git a/watcher/decision_engine/datasources/grafana.py b/watcher/decision_engine/datasources/grafana.py index 2cbff9e0f..6c736766b 100644 --- a/watcher/decision_engine/datasources/grafana.py +++ b/watcher/decision_engine/datasources/grafana.py @@ -21,6 +21,7 @@ from urllib import parse as urlparse from oslo_config import cfg from oslo_log import log +from watcher._i18n import _ from watcher.common import clients from watcher.common import exception from watcher.decision_engine.datasources import base @@ -188,6 +189,12 @@ class GrafanaHelper(base.DataSourceBase): return result + def statistic_series(self, resource=None, resource_type=None, + meter_name=None, start_time=None, end_time=None, + granularity=300): + raise NotImplementedError( + _('Grafana helper does not support statistic series method')) + def get_host_cpu_usage(self, resource, period=300, aggregate="mean", granularity=None): return self.statistic_aggregation( diff --git a/watcher/decision_engine/datasources/monasca.py b/watcher/decision_engine/datasources/monasca.py index bd141eea3..951a62835 100644 --- a/watcher/decision_engine/datasources/monasca.py +++ b/watcher/decision_engine/datasources/monasca.py @@ -21,7 +21,6 @@ import datetime from monascaclient import exc from watcher.common import clients -from watcher.common import exception from watcher.decision_engine.datasources import base @@ -90,9 +89,7 @@ class MonascaHelper(base.DataSourceBase): stop_time = datetime.datetime.utcnow() start_time = stop_time - datetime.timedelta(seconds=(int(period))) - meter = self.METRIC_MAP.get(meter_name) - if meter is None: - raise exception.MetricNotAvailable(metric=meter_name) + meter = self._get_meter(meter_name) if aggregate == 'mean': aggregate = 'avg' @@ -121,6 +118,34 @@ class MonascaHelper(base.DataSourceBase): return cpu_usage + def statistic_series(self, resource=None, resource_type=None, + meter_name=None, start_time=None, end_time=None, + granularity=300): + + meter = self._get_meter(meter_name) + + raw_kwargs = dict( + name=meter, + start_time=start_time.isoformat(), + end_time=end_time.isoformat(), + dimensions={'hostname': resource.uuid}, + statistics='avg', + group_by='*', + ) + + kwargs = {k: v for k, v in raw_kwargs.items() if k and v} + + statistics = self.query_retry( + f=self.monasca.metrics.list_statistics, **kwargs) + + result = {} + for stat in statistics: + v_index = stat['columns'].index('avg') + t_index = stat['columns'].index('timestamp') + result.update({r[t_index]: r[v_index] for r in stat['statistics']}) + + return result + def get_host_cpu_usage(self, resource, period, aggregate, granularity=None): return self.statistic_aggregation( diff --git a/watcher/tests/decision_engine/datasources/test_gnocchi_helper.py b/watcher/tests/decision_engine/datasources/test_gnocchi_helper.py index bab3b5303..bca1ca49c 100644 --- a/watcher/tests/decision_engine/datasources/test_gnocchi_helper.py +++ b/watcher/tests/decision_engine/datasources/test_gnocchi_helper.py @@ -13,7 +13,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. - +from datetime import datetime from unittest import mock from oslo_config import cfg @@ -59,6 +59,35 @@ class TestGnocchiHelper(base.BaseTestCase): ) self.assertEqual(expected_result, result) + def test_gnocchi_statistic_series(self, mock_gnocchi): + gnocchi = mock.MagicMock() + expected_result = { + "2017-02-02T09:00:00.000000": 5.5, + "2017-02-02T09:03:60.000000": 5.8 + } + + expected_measures = [ + ["2017-02-02T09:00:00.000000", 360, 5.5], + ["2017-02-02T09:03:60.000000", 360, 5.8] + ] + + gnocchi.metric.get_measures.return_value = expected_measures + mock_gnocchi.return_value = gnocchi + + start = datetime(year=2017, month=2, day=2, hour=9, minute=0) + end = datetime(year=2017, month=2, day=2, hour=9, minute=4) + + helper = gnocchi_helper.GnocchiHelper() + result = helper.statistic_series( + resource=mock.Mock(id='16a86790-327a-45f9-bc82-45839f062fdc'), + resource_type='instance', + meter_name='instance_cpu_usage', + start_time=start, + end_time=end, + granularity=360, + ) + self.assertEqual(expected_result, result) + def test_statistic_aggregation_metric_unavailable(self, mock_gnocchi): helper = gnocchi_helper.GnocchiHelper() diff --git a/watcher/tests/decision_engine/datasources/test_monasca_helper.py b/watcher/tests/decision_engine/datasources/test_monasca_helper.py index a4f45f879..71e2af41f 100644 --- a/watcher/tests/decision_engine/datasources/test_monasca_helper.py +++ b/watcher/tests/decision_engine/datasources/test_monasca_helper.py @@ -13,7 +13,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. - +from datetime import datetime from unittest import mock from oslo_config import cfg @@ -67,6 +67,43 @@ class TestMonascaHelper(base.BaseTestCase): ) self.assertEqual(0.6, result) + def test_monasca_statistic_series(self, mock_monasca): + monasca = mock.MagicMock() + expected_stat = [{ + 'columns': ['timestamp', 'avg'], + 'dimensions': { + 'hostname': 'rdev-indeedsrv001', + 'service': 'monasca'}, + 'id': '0', + 'name': 'cpu.percent', + 'statistics': [ + ['2016-07-29T12:45:00Z', 0.0], + ['2016-07-29T12:50:00Z', 0.9], + ['2016-07-29T12:55:00Z', 0.9]]}] + + expected_result = { + '2016-07-29T12:45:00Z': 0.0, + '2016-07-29T12:50:00Z': 0.9, + '2016-07-29T12:55:00Z': 0.9, + } + + monasca.metrics.list_statistics.return_value = expected_stat + mock_monasca.return_value = monasca + + start = datetime(year=2016, month=7, day=29, hour=12, minute=45) + end = datetime(year=2016, month=7, day=29, hour=12, minute=55) + + helper = monasca_helper.MonascaHelper() + result = helper.statistic_series( + resource=mock.Mock(id='NODE_UUID'), + resource_type='compute_node', + meter_name='host_cpu_usage', + start_time=start, + end_time=end, + granularity=300, + ) + self.assertEqual(expected_result, result) + def test_statistic_aggregation_metric_unavailable(self, mock_monasca): helper = monasca_helper.MonascaHelper()