Add API endpoint for listing raw event data

This change adds some of the endpoints for listing raw
event data from the database. It does not yet support
listing events by project id.

It also fixes a problem with the MongoDB driver returning
Mongo's ObjectId instances in the results of the event
query, which makes them impossible to serialize via JSON.

Change-Id: I08d122ecd2f726fb1b2880bc22e28113f6a3aeb1
Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
This commit is contained in:
Doug Hellmann 2012-07-30 17:49:33 -04:00
parent dbccbb5cb9
commit 74e381fc9d
4 changed files with 148 additions and 11 deletions

View File

@ -20,17 +20,25 @@
import flask import flask
from ceilometer.openstack.common import log
from ceilometer import storage
LOG = log.getLogger(__name__)
blueprint = flask.Blueprint('v1', __name__) blueprint = flask.Blueprint('v1', __name__)
## APIs for working with resources. ## APIs for working with resources.
@blueprint.route('/resources', defaults={'source': None}) @blueprint.route('/resources', defaults={'source': None})
@blueprint.route('/sources/<source>/resources') @blueprint.route('/sources/<source>/resources')
def list_resources(source): def list_resources(source):
resources = list(flask.request.storage_conn.get_resources(source=source)) resources = flask.request.storage_conn.get_resources(source=source)
return flask.jsonify(resources=resources) return flask.jsonify(resources=list(resources))
## APIs for working with users. ## APIs for working with users.
@ -38,5 +46,28 @@ def list_resources(source):
@blueprint.route('/users', defaults={'source': None}) @blueprint.route('/users', defaults={'source': None})
@blueprint.route('/sources/<source>/users') @blueprint.route('/sources/<source>/users')
def list_users(source): def list_users(source):
users = list(flask.request.storage_conn.get_users(source=source)) users = flask.request.storage_conn.get_users(source=source)
return flask.jsonify(users=users) return flask.jsonify(users=list(users))
## APIs for working with events.
@blueprint.route('/users/<user>')
@blueprint.route('/users/<user>/meters/<meter>')
@blueprint.route('/users/<user>/resources/<resource>')
@blueprint.route('/users/<user>/resources/<resource>/meter/<meter>')
@blueprint.route('/sources/<source>/users/<user>')
@blueprint.route('/sources/<source>/users/<user>/meters/<meter>')
@blueprint.route('/sources/<source>/users/<user>/resources/<resource>')
@blueprint.route(
'/sources/<source>/users/<user>/resources/<resource>/meter/<meter>'
)
def list_events(user, meter=None, resource=None, source=None):
f = storage.EventFilter(user=user,
source=source,
meter=meter,
resource=resource,
)
events = flask.request.storage_conn.get_raw_events(f)
return flask.jsonify(events=list(events))

View File

@ -18,6 +18,7 @@
"""MongoDB storage backend """MongoDB storage backend
""" """
import copy
import datetime import datetime
from ceilometer.openstack.common import log from ceilometer.openstack.common import log
@ -240,8 +241,11 @@ class Connection(base.Connection):
upsert=True, upsert=True,
) )
# Record the raw data for the event # Record the raw data for the event. Use a copy so we do not
self.db.meter.insert(data) # modify a data structure owned by our caller (the driver adds
# a new key '_id').
record = copy.copy(data)
self.db.meter.insert(record)
return return
def get_users(self, source=None): def get_users(self, source=None):
@ -293,6 +297,8 @@ class Connection(base.Connection):
for resource in self.db.resource.find(q): for resource in self.db.resource.find(q):
r = {} r = {}
r.update(resource) r.update(resource)
# Replace the '_id' key with 'resource_id' to meet the
# caller's expectations.
r['resource_id'] = r['_id'] r['resource_id'] = r['_id']
del r['_id'] del r['_id']
yield r yield r
@ -302,7 +308,12 @@ class Connection(base.Connection):
""" """
q = make_query_from_filter(event_filter, require_meter=False) q = make_query_from_filter(event_filter, require_meter=False)
events = self.db.meter.find(q) events = self.db.meter.find(q)
return events for e in events:
# Remove the ObjectId generated by the database when
# the event was inserted. It is an implementation
# detail that should not leak outside of the driver.
del e['_id']
yield e
def get_volume_sum(self, event_filter): def get_volume_sum(self, event_filter):
"""Return the sum of the volume field for the events """Return the sum of the volume field for the events

View File

@ -52,6 +52,8 @@ class Connection(impl_mongodb.Connection):
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
DBNAME = 'testdb'
def setUp(self): def setUp(self):
super(TestBase, self).setUp() super(TestBase, self).setUp()
self.app = flask.Flask('test') self.app = flask.Flask('test')
@ -61,17 +63,24 @@ class TestBase(unittest.TestCase):
self.conf.metering_storage_engine = 'mongodb' self.conf.metering_storage_engine = 'mongodb'
self.conf.mongodb_host = 'localhost' self.conf.mongodb_host = 'localhost'
self.conf.mongodb_port = 27017 self.conf.mongodb_port = 27017
self.conf.mongodb_dbname = 'testdb' self.conf.mongodb_dbname = self.DBNAME
self.conn = Connection(self.conf) self.conn = Connection(self.conf)
self.conn.conn.drop_database('testdb') self.conn.conn.drop_database(self.DBNAME)
self.conn.conn['testdb'] self.conn.conn[self.DBNAME]
@self.app.before_request @self.app.before_request
def attach_storage_connection(): def attach_storage_connection():
flask.request.storage_conn = self.conn flask.request.storage_conn = self.conn
return return
def tearDown(self):
self.conn.conn.drop_database(self.DBNAME)
def get(self, path): def get(self, path):
rv = self.test_app.get(path) rv = self.test_app.get(path)
try:
data = json.loads(rv.data) data = json.loads(rv.data)
except ValueError:
print 'RAW DATA:', rv
raise
return data return data

View File

@ -0,0 +1,86 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.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 raw events.
"""
import datetime
import logging
from ceilometer import counter
from ceilometer import meter
from ceilometer.tests import api as tests_api
LOG = logging.getLogger(__name__)
class TestListEvents(tests_api.TestBase):
def setUp(self):
super(TestListEvents, self).setUp()
self.counter1 = counter.Counter(
'source1',
'instance',
'cumulative',
1,
'user-id',
'project-id',
'resource-id',
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
duration=0,
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter',
}
)
msg = meter.meter_message_from_counter(self.counter1)
self.conn.record_metering_data(msg)
self.counter2 = counter.Counter(
'source2',
'instance',
'cumulative',
1,
'user-id',
'project-id',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
duration=0,
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(self.counter2)
self.conn.record_metering_data(msg2)
def test_empty(self):
data = self.get('/users/no-such-user')
self.assertEquals({'events': []}, data)
def test_with_user(self):
data = self.get('/users/user-id')
self.assertEquals(2, len(data['events']))
def test_with_source_and_user(self):
data = self.get('/sources/source1/users/user-id')
ids = [r['resource_id'] for r in data['events']]
self.assertEquals(['resource-id'], ids)
def test_with_resource(self):
data = self.get('/users/user-id/resources/resource-id')
ids = [r['resource_id'] for r in data['events']]
self.assertEquals(['resource-id'], ids)