diff options
author | Michał Jastrzębski <inc007@gmail.com> | 2014-03-13 15:48:03 +0100 |
---|---|---|
committer | Michał Jastrzębski <inc007@gmail.com> | 2014-03-27 15:04:40 +0100 |
commit | fcfffacd3b13ca295a7f2ba42176f4212c1c96fb (patch) | |
tree | 4861552deebcc0809d6e21abe48cbfbc98c5fbb6 | |
parent | b99547b4d333203ee44c9ec0b82fbd26b517454d (diff) | |
download | python-ceilometerclient-fcfffacd3b13ca295a7f2ba42176f4212c1c96fb.tar.gz |
Implementation of statistics aggregators
This corresponds to do API calls on:
/v2/meters/meter-name/statistics?aggregate.func=func-name
Usage:
aggregates = [{'func': 'cardinality', 'param': 'resource_id'}])
client.statistics.list(meter_name="instance", aggregates=aggregates)
CLI:
ceilometer statistics -m instance -a "cardinality<-resource_id"
Change-Id: I0096668585a5c7e7985973f07049eb91f44413fe
-rw-r--r-- | ceilometerclient/tests/v2/test_shell.py | 154 | ||||
-rw-r--r-- | ceilometerclient/tests/v2/test_statistics.py | 46 | ||||
-rw-r--r-- | ceilometerclient/v2/shell.py | 57 | ||||
-rw-r--r-- | ceilometerclient/v2/statistics.py | 15 |
4 files changed, 257 insertions, 15 deletions
diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 3d96593..80c57c8 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -27,6 +27,7 @@ from ceilometerclient.tests import utils from ceilometerclient.v2 import alarms from ceilometerclient.v2 import samples from ceilometerclient.v2 import shell as ceilometer_shell +from ceilometerclient.v2 import statistics class ShellAlarmStateCommandsTest(utils.BaseTestCase): @@ -634,3 +635,156 @@ rule change | {"threshold": 42.0, "evaluation_periods": 4} | 2014-03-11T16:0\ ------------+----------------------------------------------+----------------\ ------------+ ''', output.getvalue()) + + +class ShellStatisticsTest(utils.BaseTestCase): + def setUp(self): + super(ShellStatisticsTest, self).setUp() + self.cc = mock.Mock() + self.displays = { + 'duration': 'Duration', + 'duration_end': 'Duration End', + 'duration_start': 'Duration Start', + 'period': 'Period', + 'period_end': 'Period End', + 'period_start': 'Period Start', + 'groupby': 'Group By', + 'avg': 'Avg', + 'count': 'Count', + 'max': 'Max', + 'min': 'Min', + 'sum': 'Sum', + 'stddev': 'Standard deviation', + 'cardinality': 'Cardinality' + } + self.args = mock.Mock() + self.args.meter_name = 'instance' + self.args.aggregate = [] + self.args.groupby = None + self.args.query = None + + def test_statistics_list_simple(self): + samples = [ + {u'count': 135, + u'duration_start': u'2013-02-04T10:51:42', + u'min': 1.0, + u'max': 1.0, + u'duration_end': + u'2013-02-05T15:46:09', + u'duration': 1734.0, + u'avg': 1.0, + u'sum': 135.0}, + ] + fields = [ + 'period', + 'period_start', + 'period_end', + 'max', + 'min', + 'avg', + 'sum', + 'count', + 'duration', + 'duration_start', + 'duration_end', + ] + statistics_ret = [ + statistics.Statistics(mock.Mock(), sample) for sample in samples + ] + self.cc.statistics.list.return_value = statistics_ret + with mock.patch('ceilometerclient.v2.shell.utils.print_list') as pmock: + ceilometer_shell.do_statistics(self.cc, self.args) + pmock.assert_called_with( + statistics_ret, + fields, + [self.displays[f] for f in fields] + ) + + def test_statistics_list_groupby(self): + samples = [ + {u'count': 135, + u'duration_start': u'2013-02-04T10:51:42', + u'min': 1.0, + u'max': 1.0, + u'duration_end': + u'2013-02-05T15:46:09', + u'duration': 1734.0, + u'avg': 1.0, + u'sum': 135.0, + u'groupby': {u'resource_id': u'foo'} + }, + {u'count': 12, + u'duration_start': u'2013-02-04T10:51:42', + u'min': 1.0, + u'max': 1.0, + u'duration_end': + u'2013-02-05T15:46:09', + u'duration': 1734.0, + u'avg': 1.0, + u'sum': 12.0, + u'groupby': {u'resource_id': u'bar'} + }, + ] + fields = [ + 'period', + 'period_start', + 'period_end', + 'groupby', + 'max', + 'min', + 'avg', + 'sum', + 'count', + 'duration', + 'duration_start', + 'duration_end', + ] + self.args.groupby = 'resource_id' + statistics_ret = [ + statistics.Statistics(mock.Mock(), sample) for sample in samples + ] + self.cc.statistics.list.return_value = statistics_ret + with mock.patch('ceilometerclient.v2.shell.utils.print_list') as pmock: + ceilometer_shell.do_statistics(self.cc, self.args) + pmock.assert_called_with( + statistics_ret, + fields, + [self.displays[f] for f in fields], + ) + + def test_statistics_list_aggregates(self): + samples = [ + {u'aggregate': {u'cardinality/resource_id': 4.0, u'count': 2.0}, + u'count': 2, + u'duration': 0.442451, + u'duration_end': u'2014-03-12T14:00:21.774154', + u'duration_start': u'2014-03-12T14:00:21.331703', + u'groupby': None, + u'period': 0, + u'period_end': u'2014-03-12T14:00:21.774154', + u'period_start': u'2014-03-12T14:00:21.331703', + u'unit': u'instance', + }, + ] + fields = [ + 'period', + 'period_start', + 'period_end', + 'count', + 'cardinality/resource_id', + 'duration', + 'duration_start', + 'duration_end', + ] + self.args.aggregate = ['count', 'cardinality<-resource_id'] + statistics_ret = [ + statistics.Statistics(mock.Mock(), sample) for sample in samples + ] + self.cc.statistics.list.return_value = statistics_ret + with mock.patch('ceilometerclient.v2.shell.utils.print_list') as pmock: + ceilometer_shell.do_statistics(self.cc, self.args) + pmock.assert_called_with( + statistics_ret, + fields, + [self.displays.get(f, f) for f in fields], + ) diff --git a/ceilometerclient/tests/v2/test_statistics.py b/ceilometerclient/tests/v2/test_statistics.py index a9649e1..68a2672 100644 --- a/ceilometerclient/tests/v2/test_statistics.py +++ b/ceilometerclient/tests/v2/test_statistics.py @@ -21,6 +21,8 @@ qry = ('q.field=resource_id&q.field=source&q.op=&q.op=' '&q.type=&q.type=&q.value=foo&q.value=bar') period = '&period=60' groupby = '&groupby=resource_id' +aggregate_query = ("aggregate.func=cardinality&aggregate.param=resource_id" + "&aggregate.func=count") samples = [ {u'count': 135, u'duration_start': u'2013-02-04T10:51:42', @@ -56,6 +58,19 @@ groupby_samples = [ u'groupby': {u'resource_id': u'bar'} }, ] +aggregate_samples = [ + {u'aggregate': {u'cardinality/resource_id': 4.0, u'count': 2.0}, + u'count': 2, + u'duration': 0.442451, + u'duration_end': u'2014-03-12T14:00:21.774154', + u'duration_start': u'2014-03-12T14:00:21.331703', + u'groupby': None, + u'period': 0, + u'period_end': u'2014-03-12T14:00:21.774154', + u'period_start': u'2014-03-12T14:00:21.331703', + u'unit': u'instance', + }, +] fixtures = { base_url: { @@ -85,6 +100,13 @@ fixtures = { groupby_samples ), }, + '%s?%s' % (base_url, aggregate_query): + { + 'GET': ( + {}, + aggregate_samples + ), + } } @@ -156,3 +178,27 @@ class StatisticsManagerTest(utils.BaseTestCase): self.assertEqual(stats[1].count, 12) self.assertEqual(stats[0].groupby.get('resource_id'), 'foo') self.assertEqual(stats[1].groupby.get('resource_id'), 'bar') + + def test_list_by_meter_name_with_aggregates(self): + aggregates = [ + { + 'func': 'cardinality', + 'param': 'resource_id', + }, + { + 'func': 'count', + } + ] + stats = list(self.mgr.list(meter_name='instance', + aggregates=aggregates)) + expect = [ + ('GET', + '%s?%s' % (base_url, aggregate_query), {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(1, len(stats)) + self.assertEqual(2, stats[0].count) + self.assertEqual(2.0, stats[0].aggregate.get('count')) + self.assertEqual(4.0, stats[0].aggregate.get( + 'cardinality/resource_id', + )) diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 43300bc..de44c51 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -33,6 +33,13 @@ ALARM_STATES = ['ok', 'alarm', 'insufficient_data'] ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] ALARM_COMBINATION_OPERATORS = ['and', 'or'] STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] +AGGREGATES = {'avg': 'Avg', + 'count': 'Count', + 'max': 'Max', + 'min': 'Min', + 'sum': 'Sum', + 'stddev': 'Standard deviation', + 'cardinality': 'Cardinality'} OPERATORS_STRING = dict(gt='>', ge='>=', lt='<', le="<=", eq='==', ne='!=') @@ -49,28 +56,50 @@ SIMPLE_OPERATORS = ["=", "!=", "<", "<=", '>', '>='] @utils.arg('-p', '--period', metavar='<PERIOD>', help='Period in seconds over which to group samples.') @utils.arg('-g', '--groupby', metavar='<FIELD>', action='append', - help='Field for group aggregation.') + help='Field for group by.') +@utils.arg('-a', '--aggregate', metavar='<FUNC>[<-<PARAM>]', action='append', + default=[], help=('Function for data aggregation. ' + 'Available aggregates are: ' + '%s.' % ", ".join(AGGREGATES.keys()))) def do_statistics(cc, args): '''List the statistics for a meter.''' - fields = {'meter_name': args.meter, - 'q': options.cli_to_array(args.query), - 'period': args.period, - 'groupby': args.groupby} + aggregates = [] + for a in args.aggregate: + aggregates.append(dict(zip(('func', 'param'), a.split("<-")))) + api_args = {'meter_name': args.meter, + 'q': options.cli_to_array(args.query), + 'period': args.period, + 'groupby': args.groupby, + 'aggregates': aggregates} try: - statistics = cc.statistics.list(**fields) + statistics = cc.statistics.list(**api_args) except exc.HTTPNotFound: raise exc.CommandError('Samples not found: %s' % args.meter) else: - field_labels = ['Period', 'Period Start', 'Period End', - 'Count', 'Min', 'Max', 'Sum', 'Avg', - 'Duration', 'Duration Start', 'Duration End'] - fields = ['period', 'period_start', 'period_end', - 'count', 'min', 'max', 'sum', 'avg', - 'duration', 'duration_start', 'duration_end'] + fields_display = {'duration': 'Duration', + 'duration_end': 'Duration End', + 'duration_start': 'Duration Start', + 'period': 'Period', + 'period_end': 'Period End', + 'period_start': 'Period Start', + 'groupby': 'Group By'} + fields_display.update(AGGREGATES) + fields = ['period', 'period_start', 'period_end'] if args.groupby: - field_labels.append('Group By') fields.append('groupby') - utils.print_list(statistics, fields, field_labels) + if args.aggregate: + for a in aggregates: + if 'param' in a: + fields.append("%(func)s/%(param)s" % a) + else: + fields.append(a['func']) + for stat in statistics: + stat.__dict__.update(stat.aggregate) + else: + fields.extend(['max', 'min', 'avg', 'sum', 'count']) + fields.extend(['duration', 'duration_start', 'duration_end']) + cols = [fields_display.get(f, f) for f in fields] + utils.print_list(statistics, fields, cols) @utils.arg('-q', '--query', metavar='<QUERY>', diff --git a/ceilometerclient/v2/statistics.py b/ceilometerclient/v2/statistics.py index a65e69a..689992c 100644 --- a/ceilometerclient/v2/statistics.py +++ b/ceilometerclient/v2/statistics.py @@ -23,9 +23,22 @@ class Statistics(base.Resource): class StatisticsManager(base.Manager): resource_class = Statistics - def list(self, meter_name, q=None, period=None, groupby=[]): + def _build_aggregates(self, aggregates): + url_aggregates = [] + for aggregate in aggregates: + url_aggregates.append( + "aggregate.func=%(func)s" % aggregate + ) + if 'param' in aggregate: + url_aggregates.append( + "aggregate.param=%(param)s" % aggregate + ) + return url_aggregates + + def list(self, meter_name, q=None, period=None, groupby=[], aggregates=[]): p = ['period=%s' % period] if period else [] p.extend(['groupby=%s' % g for g in groupby] if groupby else []) + p.extend(self._build_aggregates(aggregates)) return self._list(options.build_url( '/v2/meters/' + meter_name + '/statistics', q, p)) |