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:
parent
ccb1b5849e
commit
4fb95009c4
@ -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())
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
87
distil/tests/unit/service/api/v2/test_health.py
Normal file
87
distil/tests/unit/service/api/v2/test_health.py
Normal 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'))
|
@ -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",
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user