Improve get_health API

Query database directly using filtering mechanism to make logic
in API layer clear and simple.

Make the API only accessible to admin only for the time being.

Currently, we only check usage collection to achieve feature parity
with current monitoring requirements. In future, we could add running
status for ERP system, etc.

Change-Id: I044cd10780f2305775d05b107be5e87c41ce7826
This commit is contained in:
Lingxian Kong 2017-05-17 16:39:18 +12:00
parent ccb1b5849e
commit 4fb95009c4
7 changed files with 168 additions and 26 deletions

View File

@ -61,6 +61,7 @@ def _get_request_args():
@rest.get('/health') @rest.get('/health')
@acl.enforce("health:get")
def health_get(): def health_get():
return api.render(health=health.get_health()) return api.render(health=health.get_health())

View File

@ -32,7 +32,7 @@ DEFAULT_OPTIONS = (
help='The listen IP for the Distil API server', help='The listen IP for the Distil API server',
), ),
cfg.ListOpt('public_api_routes', cfg.ListOpt('public_api_routes',
default=['/', '/v2/products', '/v2/health'], default=['/', '/v2/products'],
help='The list of public API routes', help='The list of public API routes',
), ),
cfg.ListOpt('ignore_tenants', cfg.ListOpt('ignore_tenants',

View File

@ -118,8 +118,8 @@ def project_get(project_id):
return IMPL.project_get(project_id) return IMPL.project_get(project_id)
def project_get_all(): def project_get_all(**filters):
return IMPL.project_get_all() return IMPL.project_get_all(**filters)
def get_last_collect(project_ids): def get_last_collect(project_ids):

View File

@ -110,6 +110,49 @@ def model_query(model, context, session=None, project_only=True):
return query return query
def apply_filters(query, model, **filters):
"""Apply filter for db query.
Sample of filters:
{
'key1': {'op': 'in', 'value': [1, 2]},
'key2': {'op': 'lt', 'value': 10},
'key3': 'value'
}
"""
filter_dict = {}
for key, criteria in filters.items():
column_attr = getattr(model, key)
if isinstance(criteria, dict):
if criteria['op'] == 'in':
query = query.filter(column_attr.in_(criteria['value']))
elif criteria['op'] == 'nin':
query = query.filter(~column_attr.in_(criteria['value']))
elif criteria['op'] == 'neq':
query = query.filter(column_attr != criteria['value'])
elif criteria['op'] == 'gt':
query = query.filter(column_attr > criteria['value'])
elif criteria['op'] == 'gte':
query = query.filter(column_attr >= criteria['value'])
elif criteria['op'] == 'lt':
query = query.filter(column_attr < criteria['value'])
elif criteria['op'] == 'lte':
query = query.filter(column_attr <= criteria['value'])
elif criteria['op'] == 'eq':
query = query.filter(column_attr == criteria['value'])
elif criteria['op'] == 'like':
like_pattern = '%{0}%'.format(criteria['value'])
query = query.filter(column_attr.like(like_pattern))
else:
filter_dict[key] = criteria
if filter_dict:
query = query.filter_by(**filter_dict)
return query
def _project_get(session, project_id): def _project_get(session, project_id):
return session.query(Tenant).filter_by(id=project_id).first() return session.query(Tenant).filter_by(id=project_id).first()
@ -139,9 +182,11 @@ def project_add(values, last_collect=None):
return project return project
def project_get_all(): def project_get_all(**filters):
session = get_session() session = get_session()
query = session.query(Tenant) query = session.query(Tenant)
query = apply_filters(query, Tenant, **filters)
return query.all() return query.all()

View File

@ -13,7 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from datetime import datetime as dt from datetime import datetime
from datetime import timedelta
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@ -25,29 +26,36 @@ CONF = cfg.CONF
def get_health(): def get_health():
health = {} """Get health status of distil.
Currently, we only check usage collection to achieve feature parity with
current monitoring requirements.
In future, we could add running status for ERP system, etc.
"""
result = {}
projects_keystone = openstack.get_projects() projects_keystone = openstack.get_projects()
project_id_list_keystone = [t['id'] for t in projects_keystone] keystone_projects = [t['id'] for t in projects_keystone]
projects = db_api.project_get_all()
# NOTE(flwang): Check the last_collected field for each tenant of Distil, threshold = datetime.utcnow() - timedelta(days=1)
# if the date is old (has not been updated more than 24 hours) and the
# tenant is still active in Keystone, we believe it should be investigated.
failed_collected_count = 0
for p in projects:
delta = (dt.now() - p.last_collected).total_seconds() // 3600
if delta >= 24 and p.id in project_id_list_keystone:
failed_collected_count += 1
# TODO(flwang): The format of health output need to be discussed so that failed_projects = db_api.project_get_all(
# we can get a stable format before it's used in monitor. id={'op': 'in', 'value': keystone_projects},
if failed_collected_count == 0: last_collected={'op': 'lte', 'value': threshold}
health['metrics_collecting'] = {'status': 'OK', )
'note': 'All tenants are synced.'}
failed_count = len(failed_projects)
if failed_count == 0:
result['usage_collection'] = {
'status': 'OK',
'msg': 'Tenant usage successfully collected and up-to-date.'
}
else: else:
note = ('Failed to collect metrics for %s projects.' % result['usage_collection'] = {
failed_collected_count) 'status': 'FAIL',
health['metrics_collecting'] = {'status': 'FAIL', 'msg': 'Failed to collect usage for %s projects.' % failed_count
'note': note} }
return health return result

View File

@ -0,0 +1,87 @@
# Copyright (C) 2017 Catalyst IT Ltd
#
# 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.
from datetime import datetime
from datetime import timedelta
import mock
from distil.db.sqlalchemy import api as db_api
from distil.service.api.v2 import health
from distil.tests.unit import base
class HealthTest(base.DistilWithDbTestCase):
@mock.patch('distil.common.openstack.get_projects')
def test_get_health_ok(self, mock_get_projects):
mock_get_projects.return_value = [
{'id': '111', 'name': 'project_1', 'description': ''},
{'id': '222', 'name': 'project_2', 'description': ''},
]
# Insert projects in the database.
project_1_collect = datetime.utcnow() - timedelta(hours=1)
db_api.project_add(
{
'id': '111',
'name': 'project_1',
'description': '',
},
project_1_collect
)
project_2_collect = datetime.utcnow() - timedelta(hours=2)
db_api.project_add(
{
'id': '222',
'name': 'project_2',
'description': '',
},
project_2_collect
)
ret = health.get_health()
self.assertEqual('OK', ret['usage_collection'].get('status'))
@mock.patch('distil.common.openstack.get_projects')
def test_get_health_fail(self, mock_get_projects):
mock_get_projects.return_value = [
{'id': '111', 'name': 'project_1', 'description': ''},
{'id': '222', 'name': 'project_2', 'description': ''},
]
# Insert projects in the database.
project_1_collect = datetime.utcnow() - timedelta(days=2)
db_api.project_add(
{
'id': '111',
'name': 'project_1',
'description': '',
},
project_1_collect
)
project_2_collect = datetime.utcnow() - timedelta(hours=25)
db_api.project_add(
{
'id': '222',
'name': 'project_2',
'description': '',
},
project_2_collect
)
ret = health.get_health()
self.assertEqual('FAIL', ret['usage_collection'].get('status'))
self.assertIn('2', ret['usage_collection'].get('msg'))

View File

@ -6,4 +6,5 @@
"rating:measurements:get": "rule:context_is_admin", "rating:measurements:get": "rule:context_is_admin",
"rating:invoices:get": "rule:context_is_admin", "rating:invoices:get": "rule:context_is_admin",
"rating:quotations:get": "rule:context_is_admin", "rating:quotations:get": "rule:context_is_admin",
"health:get": "rule:context_is_admin",
} }