Skip to content

Commit fcfffac

Browse files
committed
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
1 parent b99547b commit fcfffac

File tree

4 files changed

+257
-15
lines changed

4 files changed

+257
-15
lines changed

ceilometerclient/tests/v2/test_shell.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from ceilometerclient.v2 import alarms
2828
from ceilometerclient.v2 import samples
2929
from ceilometerclient.v2 import shell as ceilometer_shell
30+
from ceilometerclient.v2 import statistics
3031

3132

3233
class ShellAlarmStateCommandsTest(utils.BaseTestCase):
@@ -634,3 +635,156 @@ def test_query(self):
634635
------------+----------------------------------------------+----------------\
635636
------------+
636637
''', output.getvalue())
638+
639+
640+
class ShellStatisticsTest(utils.BaseTestCase):
641+
def setUp(self):
642+
super(ShellStatisticsTest, self).setUp()
643+
self.cc = mock.Mock()
644+
self.displays = {
645+
'duration': 'Duration',
646+
'duration_end': 'Duration End',
647+
'duration_start': 'Duration Start',
648+
'period': 'Period',
649+
'period_end': 'Period End',
650+
'period_start': 'Period Start',
651+
'groupby': 'Group By',
652+
'avg': 'Avg',
653+
'count': 'Count',
654+
'max': 'Max',
655+
'min': 'Min',
656+
'sum': 'Sum',
657+
'stddev': 'Standard deviation',
658+
'cardinality': 'Cardinality'
659+
}
660+
self.args = mock.Mock()
661+
self.args.meter_name = 'instance'
662+
self.args.aggregate = []
663+
self.args.groupby = None
664+
self.args.query = None
665+
666+
def test_statistics_list_simple(self):
667+
samples = [
668+
{u'count': 135,
669+
u'duration_start': u'2013-02-04T10:51:42',
670+
u'min': 1.0,
671+
u'max': 1.0,
672+
u'duration_end':
673+
u'2013-02-05T15:46:09',
674+
u'duration': 1734.0,
675+
u'avg': 1.0,
676+
u'sum': 135.0},
677+
]
678+
fields = [
679+
'period',
680+
'period_start',
681+
'period_end',
682+
'max',
683+
'min',
684+
'avg',
685+
'sum',
686+
'count',
687+
'duration',
688+
'duration_start',
689+
'duration_end',
690+
]
691+
statistics_ret = [
692+
statistics.Statistics(mock.Mock(), sample) for sample in samples
693+
]
694+
self.cc.statistics.list.return_value = statistics_ret
695+
with mock.patch('ceilometerclient.v2.shell.utils.print_list') as pmock:
696+
ceilometer_shell.do_statistics(self.cc, self.args)
697+
pmock.assert_called_with(
698+
statistics_ret,
699+
fields,
700+
[self.displays[f] for f in fields]
701+
)
702+
703+
def test_statistics_list_groupby(self):
704+
samples = [
705+
{u'count': 135,
706+
u'duration_start': u'2013-02-04T10:51:42',
707+
u'min': 1.0,
708+
u'max': 1.0,
709+
u'duration_end':
710+
u'2013-02-05T15:46:09',
711+
u'duration': 1734.0,
712+
u'avg': 1.0,
713+
u'sum': 135.0,
714+
u'groupby': {u'resource_id': u'foo'}
715+
},
716+
{u'count': 12,
717+
u'duration_start': u'2013-02-04T10:51:42',
718+
u'min': 1.0,
719+
u'max': 1.0,
720+
u'duration_end':
721+
u'2013-02-05T15:46:09',
722+
u'duration': 1734.0,
723+
u'avg': 1.0,
724+
u'sum': 12.0,
725+
u'groupby': {u'resource_id': u'bar'}
726+
},
727+
]
728+
fields = [
729+
'period',
730+
'period_start',
731+
'period_end',
732+
'groupby',
733+
'max',
734+
'min',
735+
'avg',
736+
'sum',
737+
'count',
738+
'duration',
739+
'duration_start',
740+
'duration_end',
741+
]
742+
self.args.groupby = 'resource_id'
743+
statistics_ret = [
744+
statistics.Statistics(mock.Mock(), sample) for sample in samples
745+
]
746+
self.cc.statistics.list.return_value = statistics_ret
747+
with mock.patch('ceilometerclient.v2.shell.utils.print_list') as pmock:
748+
ceilometer_shell.do_statistics(self.cc, self.args)
749+
pmock.assert_called_with(
750+
statistics_ret,
751+
fields,
752+
[self.displays[f] for f in fields],
753+
)
754+
755+
def test_statistics_list_aggregates(self):
756+
samples = [
757+
{u'aggregate': {u'cardinality/resource_id': 4.0, u'count': 2.0},
758+
u'count': 2,
759+
u'duration': 0.442451,
760+
u'duration_end': u'2014-03-12T14:00:21.774154',
761+
u'duration_start': u'2014-03-12T14:00:21.331703',
762+
u'groupby': None,
763+
u'period': 0,
764+
u'period_end': u'2014-03-12T14:00:21.774154',
765+
u'period_start': u'2014-03-12T14:00:21.331703',
766+
u'unit': u'instance',
767+
},
768+
]
769+
fields = [
770+
'period',
771+
'period_start',
772+
'period_end',
773+
'count',
774+
'cardinality/resource_id',
775+
'duration',
776+
'duration_start',
777+
'duration_end',
778+
]
779+
self.args.aggregate = ['count', 'cardinality<-resource_id']
780+
statistics_ret = [
781+
statistics.Statistics(mock.Mock(), sample) for sample in samples
782+
]
783+
self.cc.statistics.list.return_value = statistics_ret
784+
with mock.patch('ceilometerclient.v2.shell.utils.print_list') as pmock:
785+
ceilometer_shell.do_statistics(self.cc, self.args)
786+
pmock.assert_called_with(
787+
statistics_ret,
788+
fields,
789+
[self.displays.get(f, f) for f in fields],
790+
)

ceilometerclient/tests/v2/test_statistics.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
'&q.type=&q.type=&q.value=foo&q.value=bar')
2222
period = '&period=60'
2323
groupby = '&groupby=resource_id'
24+
aggregate_query = ("aggregate.func=cardinality&aggregate.param=resource_id"
25+
"&aggregate.func=count")
2426
samples = [
2527
{u'count': 135,
2628
u'duration_start': u'2013-02-04T10:51:42',
@@ -56,6 +58,19 @@
5658
u'groupby': {u'resource_id': u'bar'}
5759
},
5860
]
61+
aggregate_samples = [
62+
{u'aggregate': {u'cardinality/resource_id': 4.0, u'count': 2.0},
63+
u'count': 2,
64+
u'duration': 0.442451,
65+
u'duration_end': u'2014-03-12T14:00:21.774154',
66+
u'duration_start': u'2014-03-12T14:00:21.331703',
67+
u'groupby': None,
68+
u'period': 0,
69+
u'period_end': u'2014-03-12T14:00:21.774154',
70+
u'period_start': u'2014-03-12T14:00:21.331703',
71+
u'unit': u'instance',
72+
},
73+
]
5974
fixtures = {
6075
base_url:
6176
{
@@ -85,6 +100,13 @@
85100
groupby_samples
86101
),
87102
},
103+
'%s?%s' % (base_url, aggregate_query):
104+
{
105+
'GET': (
106+
{},
107+
aggregate_samples
108+
),
109+
}
88110
}
89111

90112

@@ -156,3 +178,27 @@ def test_list_by_meter_name_with_groupby(self):
156178
self.assertEqual(stats[1].count, 12)
157179
self.assertEqual(stats[0].groupby.get('resource_id'), 'foo')
158180
self.assertEqual(stats[1].groupby.get('resource_id'), 'bar')
181+
182+
def test_list_by_meter_name_with_aggregates(self):
183+
aggregates = [
184+
{
185+
'func': 'cardinality',
186+
'param': 'resource_id',
187+
},
188+
{
189+
'func': 'count',
190+
}
191+
]
192+
stats = list(self.mgr.list(meter_name='instance',
193+
aggregates=aggregates))
194+
expect = [
195+
('GET',
196+
'%s?%s' % (base_url, aggregate_query), {}, None),
197+
]
198+
self.assertEqual(expect, self.api.calls)
199+
self.assertEqual(1, len(stats))
200+
self.assertEqual(2, stats[0].count)
201+
self.assertEqual(2.0, stats[0].aggregate.get('count'))
202+
self.assertEqual(4.0, stats[0].aggregate.get(
203+
'cardinality/resource_id',
204+
))

ceilometerclient/v2/shell.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt']
3434
ALARM_COMBINATION_OPERATORS = ['and', 'or']
3535
STATISTICS = ['max', 'min', 'avg', 'sum', 'count']
36+
AGGREGATES = {'avg': 'Avg',
37+
'count': 'Count',
38+
'max': 'Max',
39+
'min': 'Min',
40+
'sum': 'Sum',
41+
'stddev': 'Standard deviation',
42+
'cardinality': 'Cardinality'}
3643
OPERATORS_STRING = dict(gt='>', ge='>=',
3744
lt='<', le="<=",
3845
eq='==', ne='!=')
@@ -49,28 +56,50 @@
4956
@utils.arg('-p', '--period', metavar='<PERIOD>',
5057
help='Period in seconds over which to group samples.')
5158
@utils.arg('-g', '--groupby', metavar='<FIELD>', action='append',
52-
help='Field for group aggregation.')
59+
help='Field for group by.')
60+
@utils.arg('-a', '--aggregate', metavar='<FUNC>[<-<PARAM>]', action='append',
61+
default=[], help=('Function for data aggregation. '
62+
'Available aggregates are: '
63+
'%s.' % ", ".join(AGGREGATES.keys())))
5364
def do_statistics(cc, args):
5465
'''List the statistics for a meter.'''
55-
fields = {'meter_name': args.meter,
56-
'q': options.cli_to_array(args.query),
57-
'period': args.period,
58-
'groupby': args.groupby}
66+
aggregates = []
67+
for a in args.aggregate:
68+
aggregates.append(dict(zip(('func', 'param'), a.split("<-"))))
69+
api_args = {'meter_name': args.meter,
70+
'q': options.cli_to_array(args.query),
71+
'period': args.period,
72+
'groupby': args.groupby,
73+
'aggregates': aggregates}
5974
try:
60-
statistics = cc.statistics.list(**fields)
75+
statistics = cc.statistics.list(**api_args)
6176
except exc.HTTPNotFound:
6277
raise exc.CommandError('Samples not found: %s' % args.meter)
6378
else:
64-
field_labels = ['Period', 'Period Start', 'Period End',
65-
'Count', 'Min', 'Max', 'Sum', 'Avg',
66-
'Duration', 'Duration Start', 'Duration End']
67-
fields = ['period', 'period_start', 'period_end',
68-
'count', 'min', 'max', 'sum', 'avg',
69-
'duration', 'duration_start', 'duration_end']
79+
fields_display = {'duration': 'Duration',
80+
'duration_end': 'Duration End',
81+
'duration_start': 'Duration Start',
82+
'period': 'Period',
83+
'period_end': 'Period End',
84+
'period_start': 'Period Start',
85+
'groupby': 'Group By'}
86+
fields_display.update(AGGREGATES)
87+
fields = ['period', 'period_start', 'period_end']
7088
if args.groupby:
71-
field_labels.append('Group By')
7289
fields.append('groupby')
73-
utils.print_list(statistics, fields, field_labels)
90+
if args.aggregate:
91+
for a in aggregates:
92+
if 'param' in a:
93+
fields.append("%(func)s/%(param)s" % a)
94+
else:
95+
fields.append(a['func'])
96+
for stat in statistics:
97+
stat.__dict__.update(stat.aggregate)
98+
else:
99+
fields.extend(['max', 'min', 'avg', 'sum', 'count'])
100+
fields.extend(['duration', 'duration_start', 'duration_end'])
101+
cols = [fields_display.get(f, f) for f in fields]
102+
utils.print_list(statistics, fields, cols)
74103

75104

76105
@utils.arg('-q', '--query', metavar='<QUERY>',

ceilometerclient/v2/statistics.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,22 @@ def __repr__(self):
2323
class StatisticsManager(base.Manager):
2424
resource_class = Statistics
2525

26-
def list(self, meter_name, q=None, period=None, groupby=[]):
26+
def _build_aggregates(self, aggregates):
27+
url_aggregates = []
28+
for aggregate in aggregates:
29+
url_aggregates.append(
30+
"aggregate.func=%(func)s" % aggregate
31+
)
32+
if 'param' in aggregate:
33+
url_aggregates.append(
34+
"aggregate.param=%(param)s" % aggregate
35+
)
36+
return url_aggregates
37+
38+
def list(self, meter_name, q=None, period=None, groupby=[], aggregates=[]):
2739
p = ['period=%s' % period] if period else []
2840
p.extend(['groupby=%s' % g for g in groupby] if groupby else [])
41+
p.extend(self._build_aggregates(aggregates))
2942
return self._list(options.build_url(
3043
'/v2/meters/' + meter_name + '/statistics',
3144
q, p))

0 commit comments

Comments
 (0)