Implement /meters to make discovery "nicer" from the client

The point of this api is to make discovery (esp. from a casual user)
easier. So you don't really want to dump all the raw samples out
just to see what is there. So instead "ceilometer meter-list"
will GET /v1/meters (or /{proj|user|source}/{id}/meters) and
this will just return a description (name, type, resource, user, etc)
of the available meters, not each sample point. After this you will probably
go and look at the samples that you are actually interested in.

It is a kind of dynamic version of doc/source/measurements.rst

Change-Id: I58f2757874ab151632b6d87043d6327104c5b65c
This commit is contained in:
Angus Salkeld 2012-12-03 14:16:57 +11:00
parent 36eccfb76a
commit b67d2c2dfb
9 changed files with 353 additions and 6 deletions

View File

@ -40,10 +40,11 @@
#
# [ ] /resources/<resource> -- metadata
#
# [ ] /projects/<project>/meters -- list of meters reporting for parent obj
# [ ] /resources/<resource>/meters -- list of meters reporting for parent obj
# [ ] /sources/<source>/meters -- list of meters reporting for parent obj
# [ ] /users/<user>/meters -- list of meters reporting for parent obj
# [x] /meters -- list of meters
# [x] /projects/<project>/meters -- list of meters reporting for parent obj
# [x] /resources/<resource>/meters -- list of meters reporting for parent obj
# [x] /sources/<source>/meters -- list of meters reporting for parent obj
# [x] /users/<user>/meters -- list of meters reporting for parent obj
#
# [x] /projects/<project>/meters/<meter> -- events
# [x] /resources/<resource>/meters/<meter> -- events
@ -99,6 +100,57 @@ def request_wants_html():
flask.request.accept_mimetypes['application/json']
## APIs for working with meters.
@blueprint.route('/meters')
def list_meters_all():
"""Return a list of meters.
"""
meters = flask.request.storage_conn.get_meters()
return flask.jsonify(meters=list(meters))
@blueprint.route('/resources/<resource>/meters')
def list_meters_by_resource(resource):
"""Return a list of meters by resource.
:param resource: The ID of the resource.
"""
meters = flask.request.storage_conn.get_meters(resource=resource)
return flask.jsonify(meters=list(meters))
@blueprint.route('/users/<user>/meters')
def list_meters_by_user(user):
"""Return a list of meters by user.
:param user: The ID of the owning user.
"""
meters = flask.request.storage_conn.get_meters(user=user)
return flask.jsonify(meters=list(meters))
@blueprint.route('/projects/<project>/meters')
def list_meters_by_project(project):
"""Return a list of meters by project.
:param project: The ID of the owning project.
"""
meters = flask.request.storage_conn.get_meters(project=project)
return flask.jsonify(meters=list(meters))
@blueprint.route('/sources/<source>/meters')
def list_meters_by_source(source):
"""Return a list of meters by source.
:param source: The ID of the owning source.
"""
meters = flask.request.storage_conn.get_meters(source=source)
return flask.jsonify(meters=list(meters))
## APIs for working with resources.
def _list_resources(source=None, user=None, project=None):

View File

@ -133,3 +133,14 @@ class Connection(object):
( datetime.datetime(), datetime.datetime() )
"""
@abc.abstractmethod
def get_meters(self, user=None, project=None, resource=None, source=None):
"""Return a list of meters.
{'resource_id': UUID string for the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
'name': The name of the meter,
'type': The meter type (gauge, counter, diff),
}
"""

View File

@ -80,6 +80,22 @@ class Connection(base.Connection):
:param end_timestamp: Optional modified timestamp end range.
"""
def get_meters(self, user=None, project=None, resource=None, source=None):
"""Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter,
'type': type of the meter (guage, counter),
'resource_id': UUID of the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
}
:param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource.
:param resource: Optional ID of the resource.
:param source: Optional source filter.
"""
def get_raw_events(self, event_filter):
"""Return an iterable of raw event data as created by
:func:`ceilometer.meter.meter_message_from_counter`.

View File

@ -22,7 +22,6 @@ import copy
import datetime
from ceilometer.openstack.common import log
from ceilometer.openstack.common import cfg
from ceilometer.storage import base
import bson.code
@ -348,6 +347,40 @@ class Connection(base.Connection):
del r['_id']
yield r
def get_meters(self, user=None, project=None, resource=None, source=None):
"""Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter,
'type': type of the meter (guage, counter),
'resource_id': UUID of the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
}
:param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource.
:param resource: Optional ID of the resource.
:param source: Optional source filter.
"""
q = {}
if user is not None:
q['user_id'] = user
if project is not None:
q['project_id'] = project
if resource is not None:
q['_id'] = resource
if source is not None:
q['source'] = source
for r in self.db.resource.find(q):
for r_meter in r['meter']:
m = {}
m['name'] = r_meter['counter_name']
m['type'] = r_meter['counter_type']
m['resource_id'] = r['_id']
m['project_id'] = r['project_id']
m['user_id'] = r['user_id']
yield m
def get_raw_events(self, event_filter):
"""Return an iterable of raw event data as created by
:func:`ceilometer.meter.meter_message_from_counter`.

View File

@ -19,7 +19,7 @@
import copy
import datetime
from ceilometer.openstack.common import cfg, log, timeutils
from ceilometer.openstack.common import log
from ceilometer.storage import base
from ceilometer.storage.sqlalchemy.models import Meter, Project, Resource
from ceilometer.storage.sqlalchemy.models import Source, User
@ -257,6 +257,48 @@ class Connection(base.Connection):
del r['meters']
yield r
def get_meters(self, user=None, project=None, source=None,
resource=None):
"""Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter,
'type': type of the meter (guage, counter),
'resource_id': UUID of the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
}
:param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource.
:param resource: Optional ID of the resource.
:param source: Optional source filter.
"""
query = model_query(Resource, session=self.session)
if user is not None:
query = query.filter(Resource.user_id == user)
if source is not None:
query = query.filter(Resource.sources.any(id=source))
if resource:
query = query.filter(Resource.id == resource)
if project is not None:
query = query.filter(Resource.project_id == project)
query = query.options(
sqlalchemy_session.sqlalchemy.orm.joinedload('meters'))
for resource in query.all():
meter_names = set()
for meter in resource.meters:
if meter.counter_name in meter_names:
continue
meter_names.add(meter.counter_name)
m = {}
m['resource_id'] = resource.id
m['project_id'] = resource.project_id
m['user_id'] = resource.user_id
m['name'] = meter.counter_name
m['type'] = meter.counter_type
yield m
def get_raw_events(self, event_filter):
"""Return an iterable of raw event data as created by
:func:`ceilometer.meter.meter_message_from_counter`.

View File

@ -117,6 +117,18 @@ storage.objects.size Gauge bytes store ID Total size of stor
storage.objects.containers Gauge containers store ID Number of containers
========================== ========== ========== ======== ==================================================
Dynamically retrieving the Meters via ceilometer client
=======================================================
ceilometer meter-list -s openstack
+------------+-------+--------------------------------------+---------+----------------------------------+
| Name | Type | Resource ID | User ID | Project ID |
+------------+-------+--------------------------------------+---------+----------------------------------+
| image | gauge | 09e84d97-8712-4dd2-bcce-45970b2430f7 | | 57cf6d93688e4d39bf2fe3d3c03eb326 |
The above command will retrieve the available meters that can be queried on
given the actual resource instances available.
Naming convention
=================
If you plan on adding meters, please follow the convention bellow:

View File

@ -0,0 +1,155 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2012 Red Hat, Inc.
#
# Author: Angus Salkeld <asalkeld@redhat.com>
#
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Test listing meters.
"""
import datetime
import logging
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from ceilometer.tests import api as tests_api
LOG = logging.getLogger(__name__)
class TestListEmptyMeters(tests_api.TestBase):
def test_empty(self):
data = self.get('/meters')
self.assertEquals({'meters': []}, data)
class TestListMeters(tests_api.TestBase):
def setUp(self):
super(TestListMeters, self).setUp()
for cnt in [
counter.Counter(
'meter.test',
'cumulative',
1,
'user-id',
'project-id',
'resource-id',
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter',
}),
counter.Counter(
'meter.test',
'cumulative',
3,
'user-id',
'project-id',
'resource-id',
timestamp=datetime.datetime(2012, 7, 2, 11, 40),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter',
}),
counter.Counter(
'meter.mine',
'gauge',
1,
'user-id',
'project-id',
'resource-id2',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}),
counter.Counter(
'meter.test',
'cumulative',
1,
'user-id2',
'project-id2',
'resource-id3',
timestamp=datetime.datetime(2012, 7, 2, 10, 42),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter3',
}),
counter.Counter(
'meter.mine',
'gauge',
1,
'user-id4',
'project-id2',
'resource-id4',
timestamp=datetime.datetime(2012, 7, 2, 10, 43),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter4',
})]:
msg = meter.meter_message_from_counter(cnt,
cfg.CONF.metering_secret,
'test_list_resources')
self.conn.record_metering_data(msg)
def test_list_meters(self):
data = self.get('/meters')
self.assertEquals(4, len(data['meters']))
self.assertEquals(set(r['resource_id'] for r in data['meters']),
set(['resource-id',
'resource-id2',
'resource-id3',
'resource-id4']))
self.assertEquals(set(r['name'] for r in data['meters']),
set(['meter.test',
'meter.mine']))
def test_with_resource(self):
data = self.get('/resources/resource-id/meters')
ids = set(r['name'] for r in data['meters'])
self.assertEquals(set(['meter.test']), ids)
def test_with_source(self):
data = self.get('/sources/test_list_resources/meters')
ids = set(r['resource_id'] for r in data['meters'])
self.assertEquals(set(['resource-id',
'resource-id2',
'resource-id3',
'resource-id4']), ids)
def test_with_source_non_existent(self):
data = self.get('/sources/test_list_resources_dont_exist/meters')
self.assertEquals(data['meters'], [])
def test_with_user(self):
data = self.get('/users/user-id/meters')
nids = set(r['name'] for r in data['meters'])
self.assertEquals(set(['meter.mine', 'meter.test']), nids)
rids = set(r['resource_id'] for r in data['meters'])
self.assertEquals(set(['resource-id', 'resource-id2']), rids)
def test_with_user_non_existent(self):
data = self.get('/users/user-id-foobar123/meters')
self.assertEquals(data['meters'], [])
def test_with_project(self):
data = self.get('/projects/project-id2/meters')
ids = set(r['resource_id'] for r in data['meters'])
self.assertEquals(set(['resource-id3', 'resource-id4']), ids)
def test_with_project_non_existent(self):
data = self.get('/projects/jd-was-here/meters')
self.assertEquals(data['meters'], [])

View File

@ -290,6 +290,18 @@ class MeterTest(MongoDBEngineTestBase):
meter = self.db.meter.find_one()
assert meter is not None
def test_get_meters(self):
results = list(self.conn.get_meters())
assert len(results) == 4
def test_get_meters_by_user(self):
results = list(self.conn.get_meters(user='user-id'))
assert len(results) == 1
def test_get_meters_by_project(self):
results = list(self.conn.get_meters(project='project-id'))
assert len(results) == 2
def test_get_raw_events_by_user(self):
f = storage.EventFilter(user='user-id')
results = list(self.conn.get_raw_events(f))

View File

@ -326,6 +326,20 @@ class MeterTest(SQLAlchemyEngineTestBase):
meter = self.session.query(Meter).first()
assert meter is not None
def test_get_meters(self):
results = list(self.conn.get_meters())
assert len(results) == 4
def test_get_meters_by_user(self):
results = list(self.conn.get_meters(user='user-id'))
assert len(results) == 1
def test_get_meters_by_project(self):
results = list(self.conn.get_meters(project='project-id'))
for r in results:
print r
assert len(results) == 2
def test_get_raw_events_by_user(self):
f = storage.EventFilter(user='user-id')
results = list(self.conn.get_raw_events(f))